Repository navigation
API Capabilities
Caligrafy Quill 2.26.0. Generated from the source by
php qa/build-api-reference.php; the guides explain how to use these classes, this reference says exactly what each public method takes and returns. Back to the API reference.
Build once: services, the capability registry, errors, the container, personal access tokens and the audit log.
| Class | What it is |
|---|---|
Caligrafy\Service |
Extend it in application/services/, declare the inputs and implement handle() |
Caligrafy\Capability |
#[Capability(name: 'create_book', description: 'Adds a book to the catalog', scope: 'books:write')] on a class that extends Caligrafy\Service. |
Caligrafy\Capabilities |
A capability is a class of application/services/ that extends Service and carries #[Capability(...)]. |
Caligrafy\ServiceError |
A service throws it to refuse a call. |
Caligrafy\Container |
Capabilities are made by the container, so a service asks for what it uses in its constructor |
Caligrafy\Tokens |
A token is cq_<id>_<secret>: shown once when it is created, stored only as a SHA-256 hash in caligrafy_tokens with a name, an optional owner (users.id), its scopes, an optional expiry, the last use and a revocation ... |
Caligrafy\Audit |
One row per capability call that changes data, and per refused call: when, who (a user, a token, the CLI), through which channel, which capability and the outcome. |
namespace Caligrafy;
abstract class ServiceSource: framework/core/Service.php:34
Class Service Extend it in application/services/, declare the inputs and implement handle():
#[Capability(name: 'create_book', description: 'Adds a book', scope: 'books:write')]
class CreateBook extends Service {
public $rules = ['title' => 'required|max_len,255', 'author_id' => 'required|integer'];
public $filters = ['title' => 'trim|sanitize_string'];
public $descriptions = ['title' => 'The title of the book'];
public function handle(array $input, Context $context) {
return (new Book('books'))->create($input);
}
}
- $rules: GUMP validation rules per input. Only the inputs listed here reach handle(); use '' for an input without a rule. They also give the JSON Schema that agents and clients see (Capabilities::schema()). A file input takes the rule 'file' (2.19.0): 'cover' => 'required|file|extension,png;jpg|max_size,2mb'. Web pages and REST save the uploaded file and give handle() its public URL; MCP and the command line send a URL or a path (Caligrafy\Uploads).
- $filters: GUMP filters applied before validation.
- handle() returns data (an array, a model, a list of models, a scalar) and throws ServiceError to refuse. It never renders a view, redirects, prints or reads the request or the session: the context says who calls.
-
public $rules = array(): array input => GUMP rules -
public $filters = array(): array input => GUMP filters -
public $descriptions = array(): array input => description, for the schema
abstract public function handle(array $input, Context $context)Does the work
Parameters
-
$inputarray: the validated and filtered inputs (only those of $rules) -
$contextContext: who calls, through which channel
Returns mixed: the result
Source: framework/core/Service.php:51
public function validate(array $input)Keeps the declared inputs, filters them and validates them
Parameters
-
$inputarray
Returns array: the clean inputs
- @throws ServiceError invalid, with a message per field
Source: framework/core/Service.php:58
protected function capability($name, array $input, Context $context)Calls another capability as the same person (2.21.0)
Parameters
$name-
$inputarray -
$contextContext
Source: framework/core/Service.php:99
public function run(array $input, ?Context $context = null)No doc comment.
Parameters
-
$inputarray -
$context?Context, defaultnull
Source: framework/core/Service.php:104
namespace Caligrafy;
class CapabilitySource: framework/core/Capability.php:17
Attribute Capability #[Capability(name: 'create_book', description: 'Adds a book to the catalog', scope: 'books:write')] on a class that extends Caligrafy\Service. Its name is how REST, MCP, the JS client and the CLI call it.
public function __construct(
public string $name,
public string $description = '',
public string $scope = '',
public int $level = 0,
public bool $readOnly = false,
public bool $destructive = false,
public bool $public = false,
public bool $mcp = true,
public bool $webOnly = false,
public bool $async = false,
public int $cache = 0
)Parameters
-
$namestring: snake_case, unique in the application (create_book) -
$descriptionstring, default'': one or two sentences: what it does and returns (shown to agents as the tool description) -
$scopestring, default'': the token scope it needs (books:write); '' = none for a read-only one, its own name for a write one (2.26.0: a token needs the scope create_book, or *; TOKEN_DEFAULT_SCOPES=false, deprecated: none) -
$levelint, default0: the minimum value of the users permission column (0 = any signed-in user) -
$readOnlybool, defaultfalse: true when it changes nothing (not audited when it succeeds; agents may call it freely) -
$destructivebool, defaultfalse: true when it deletes or overwrites data (agents ask before calling it) -
$publicbool, defaultfalse: true when anyone may call it without signing in or a token -
$mcpbool, defaulttrue: false keeps it out of the MCP tools (one only a browser can use, such as a push subscription) -
$webOnlybool, defaultfalse: true (2.21.0): only Route::action() reaches it: it may read the session and sign people in -
$asyncbool, defaultfalse: true (2.23.0): Snap cards run it in the background as a task; REST, MCP, pages and call run it directly -
$cacheint, default0: seconds (2.26.0): a GET of it over REST by a visitor with no session cookie and no token may be kept by shared caches (the production image's own cache, a CDN) for that long; needs readOnly and public, and an answer that is the same for every visitor
Source: framework/core/Capability.php:35
namespace Caligrafy;
class CapabilitiesSource: framework/core/Capabilities.php:23
Class Capabilities A capability is a class of application/services/ that extends Service and carries #[Capability(...)]. Every channel calls it through call(), which always does the same, in order:
- finds it (not_found)
- checks the context: public, or signed in / a token (unauthenticated), with the scope and level (forbidden); a token needs the declared scope, or for a write capability that declares none its own name (scopeOf(), 2.26.0)
- keeps, filters and validates the declared inputs (invalid)
- runs handle()
- writes the audit log: every refusal, and every success of a capability that is not read-only and returns plain data (models become arrays of their columns).
-
public static $defaultScopes = null: bool|null whether a write capability declared without a scope needs the scope named after it from a token (2.26.0): null until the first use reads TOKEN_DEFAULT_SCOPES (false, deprecated: any token, as before)
Capabilities::scopeOf()Capabilities::unscopedWrites()Capabilities::register()Capabilities::all()Capabilities::get()Capabilities::available()Capabilities::call()Capabilities::schema()Capabilities::schemaFromRules()Capabilities::describe()Capabilities::plain()Capabilities::reset()
public static function scopeOf(Capability $capability)The scope a token needs to call a capability (2.26.0): the one it declares; for a write capability (not readOnly, not public) declared without one, its own name (create_book), which a token grants by that name or by *. Read-only and public capabilities without a scope need none. Signed-in users and the CLI never need a scope.
Parameters
-
$capabilityCapability
Source: framework/core/Capabilities.php:42
public static function unscopedWrites()The write capabilities declared without a scope, each with the scope a token now needs to call them (doctor)
Returns array: name => scope
Source: framework/core/Capabilities.php:60
public static function register($class)Registers a service class that is not in application/services/
Parameters
$class
Source: framework/core/Capabilities.php:75
public static function all()Every capability, by name
Returns array: name => ['class' => string, 'capability' => Capability]
Source: framework/core/Capabilities.php:85
public static function get($name)One capability (null when there is none of that name)
Parameters
$name
Source: framework/core/Capabilities.php:119
public static function available(Context $context)The capabilities a context may call (MCP lists only these)
Parameters
-
$contextContext
Source: framework/core/Capabilities.php:127
public static function call($name, array $input, Context $context, ?Uploads $uploads = null)Calls a capability
Parameters
-
$namestring: the capability name -
$inputarray: the inputs (extra keys are ignored) -
$contextContext: who calls, through which channel -
$uploads?Uploads, defaultnull: the files of the request (web and REST): saved once the caller is allowed, given to the capability as their URLs, deleted again when the call fails
Returns mixed: plain data
- @throws ServiceError
Source: framework/core/Capabilities.php:145
public static function schema($name)The JSON Schema of a capability's inputs, from its GUMP rules
Parameters
$name
Source: framework/core/Capabilities.php:195
public static function schemaFromRules(array $rules, array $descriptions = array())JSON Schema (draft 2020-12 subset) of GUMP rules
Parameters
-
$rulesarray: input => 'required|max_len,100|integer' -
$descriptionsarray, defaultarray(): input => description
Source: framework/core/Capabilities.php:210
public static function describe(?Context $context = null)The description of every capability, as a list (php caligrafer.php capabilities, the REST listing)
Parameters
-
$context?Context, defaultnull: only the capabilities this context may call
Source: framework/core/Capabilities.php:273
public static function plain($value)Plain data: models become arrays of their public columns, recursively
Parameters
$value
Source: framework/core/Capabilities.php:291
public static function reset()Forgets the registry (tests)
Source: framework/core/Capabilities.php:350
namespace Caligrafy;
class ServiceError extends \ExceptionSource: framework/core/ServiceError.php:16
Class ServiceError A service throws it to refuse a call. Each channel turns it into its own form: an HTTP status and a JSON error for REST, a tool error for MCP, the errors of a form for web pages.
STATUSES = array('invalid' => 422, 'unauthenticated' => 401, 'forbidden' => 403, 'not_found' => 404, 'conflict' => 409)
-
public $kind: string invalid, unauthenticated, forbidden, not_found or conflict -
public $fields: array field => message, for invalid input
->__construct()ServiceError::invalid()ServiceError::unauthenticated()ServiceError::forbidden()ServiceError::notFound()ServiceError::conflict()->status()->toArray()
public function __construct($kind, $message = '', array $fields = array())No doc comment.
Parameters
$kind-
$message, default'' -
$fieldsarray, defaultarray()
Source: framework/core/ServiceError.php:26
public static function invalid(array $fields, $message = 'Some of the inputs are invalid')No doc comment.
Parameters
-
$fieldsarray -
$message, default'Some of the inputs are invalid'
Source: framework/core/ServiceError.php:36
public static function unauthenticated($message = 'Sign in or send a personal access token')No doc comment.
Parameters
-
$message, default'Sign in or send a personal access token'
Source: framework/core/ServiceError.php:41
public static function forbidden($message = 'You are not allowed to do this')No doc comment.
Parameters
-
$message, default'You are not allowed to do this'
Source: framework/core/ServiceError.php:46
public static function notFound($message = 'Not found')No doc comment.
Parameters
-
$message, default'Not found'
Source: framework/core/ServiceError.php:51
public static function conflict($message)No doc comment.
Parameters
$message
Source: framework/core/ServiceError.php:56
public function status()The HTTP status of this error
Source: framework/core/ServiceError.php:64
public function toArray(){"error": {"code": "invalid", "message": "...", "fields": {...}}}
Source: framework/core/ServiceError.php:72
namespace Caligrafy;
class ContainerSource: framework/core/Container.php:25
Class Container (2.18.0) Capabilities are made by the container, so a service asks for what it uses in its constructor:
public function __construct(private Mailer $mailer) {}
A class the container has no binding for is built with its constructor's parameters resolved the same way (a class or interface type -> Container::get, else the parameter's default). Bindings go in application/config/container.php:
Container::singleton(PaymentGateway::class, fn () => new StripeGateway(getenv('STRIPE_SECRET')));
Container::bind(Clock::class, SystemClock::class);
A test swaps one with Container::instance(Mailer::class, $fake), then Container::reset() forgets it.
FILE = 'config/container.php'
Container::bind()Container::singleton()Container::instance()Container::has()Container::get()Container::make()Container::boot()Container::forgetInstances()Container::keepInstances()Container::reset()
public static function bind($id, $factory = null)A new object each time: $factory is a callable returning it, or a class name (an interface bound to a class)
Parameters
$id-
$factory, defaultnull
Source: framework/core/Container.php:38
public static function singleton($id, $factory = null)One object for the whole request, made the first time it is asked for
Parameters
$id-
$factory, defaultnull
Source: framework/core/Container.php:46
public static function instance($id, $object)This object for $id (a test's fake, for example)
Parameters
$id$object
Source: framework/core/Container.php:54
public static function has($id)Whether $id has a binding or an instance
Parameters
$id
Source: framework/core/Container.php:64
public static function get($id)The object for $id: its instance, its binding, or the class built with its dependencies
Parameters
$id
Source: framework/core/Container.php:73
public static function make($class, array $parameters = array())Builds $class (never a shared instance): the constructor parameters named in $parameters, then by type, then their defaults
Parameters
$class-
$parametersarray, defaultarray()
Source: framework/core/Container.php:96
public static function boot()Loads application/config/container.php once (the web front controller does it after config.php; the CLI and tests the first time the container is used)
Source: framework/core/Container.php:132
public static function forgetInstances()Forgets the objects made so far, keeping the bindings (2.26.0: between two requests of the async server, so no shared instance carries one caller's state to the next)
Source: framework/core/Container.php:147
public static function keepInstances()Keeps the instances made so far (the ones application/config/container.php registered at boot): forgetInstances() goes back to them
Source: framework/core/Container.php:157
public static function reset()Forgets every binding and instance (tests); the bindings file is read again on next use
Source: framework/core/Container.php:165
namespace Caligrafy;
class TokensSource: framework/core/Tokens.php:20
Class Tokens
A token is cq_<id>_<secret>: shown once when it is created, stored only as a SHA-256 hash in caligrafy_tokens
with a name, an optional owner (users.id), its scopes, an optional expiry, the last use and a revocation date.
Scopes are <resource>:<action> strings (books:read, books:write). A token scope * grants everything and
books:* grants every action on books. A capability's own name (create_book) is a scope too (2.26.0): the one a write
capability declared without a scope needs (Capabilities::scopeOf()). Tables: framework/settings/platform.sql (php caligrafer.php platform install).
TABLE = 'caligrafy_tokens'PREFIX = 'cq_'
Tokens::create()Tokens::verify()Tokens::expired()Tokens::expiresAt()Tokens::revoke()Tokens::all()Tokens::installed()Tokens::looksLikeToken()Tokens::parse()Tokens::allows()Tokens::normalizeScopes()
public static function create($name, $scopes = array(), $userId = null, $expiresAt = null)Creates a token
Parameters
-
$namestring: what the token is for ("Claude Code on my laptop") -
$scopesarray, defaultarray(): the scopes it grants -
$userIdint|null, defaultnull: the user it acts for (null: the application itself) -
$expiresAtint|string|null, defaultnull: a Unix time (time() + 86400 * 90), or a local 'Y-m-d H:i:s' (null: never expires)
Returns array: ['id' => ..., 'token' => 'cq_...']: the token is never shown again
Source: framework/core/Tokens.php:33
public static function verify($token)Checks a token and records its use
Parameters
-
$tokenstring: the token as sent (Authorization: Bearer cq_...)
Returns array|null: the token row (id, name, scopes as an array, user_id...) or null when unknown, revoked or expired
Source: framework/core/Tokens.php:54
public static function expired($expiresAt, $now = null)Whether an expiry date (local 'Y-m-d H:i:s', SERVER_TIMEZONE) has passed
Parameters
-
$expiresAtstring|null: the expires_at column (null: never expires) -
$nowint|null, defaultnull: a Unix time (null: now)
Source: framework/core/Tokens.php:79
public static function expiresAt($expiresAt)The expires_at value of an expiry given as a Unix time. Dates are stored in local time (SERVER_TIMEZONE), and when the clocks fall back the repeated hour names two instants: strtotime() reads its first one. A token created at 01:21 EDT to expire an hour later (01:21 EST) would be refused at once, so an instant of the second occurrence is stored as the first local time after the repeated hour (02:00): it never stops early, at worst an hour late.
Parameters
-
$expiresAtint|string|null: a Unix time, a local 'Y-m-d H:i:s' (kept as given) or null (never expires)
Returns string|null
Source: framework/core/Tokens.php:96
public static function revoke($id)Revokes a token
Parameters
$id
Returns bool: true when a token was revoked
Source: framework/core/Tokens.php:112
public static function all()Lists the tokens (never their secret)
Returns array: rows, newest first
Source: framework/core/Tokens.php:126
public static function installed()Whether the framework tables exist (php caligrafer.php platform install)
Source: framework/core/Tokens.php:139
public static function looksLikeToken($token)Whether a request carries a token of this format (as opposed to a legacy API key)
Parameters
$token
Source: framework/core/Tokens.php:152
public static function parse($token)The id of a well-formed token, null otherwise
Parameters
$token
Source: framework/core/Tokens.php:160
public static function allows($granted, $required)Whether granted scopes cover a required scope: exact, <resource>:* or *. An empty requirement is always met.
Parameters
$granted$required
Source: framework/core/Tokens.php:168
public static function normalizeScopes($scopes)Scopes as a sorted list of unique, valid strings (from an array or a space/comma separated string)
Parameters
$scopes
Source: framework/core/Tokens.php:188
namespace Caligrafy;
class AuditSource: framework/core/Audit.php:17
Class Audit One row per capability call that changes data, and per refused call: when, who (a user, a token, the CLI), through which channel, which capability and the outcome. Inputs are never stored. Without the table (php caligrafer.php platform install) nothing is written and nothing fails.
TABLE = 'caligrafy_audit'
public static function record(Context $context, $capability, $outcome, $detail = null)Records one call
Parameters
-
$contextContext: who called and through which channel -
$capabilitystring: the capability name -
$outcomestring: ok, invalid, unauthenticated, forbidden, not_found, conflict, error -
$detailstring|null, defaultnull: a short note (an error message), at most 500 characters
Returns bool: whether the row was written
Source: framework/core/Audit.php:29
public static function latest($limit = 50, $capability = null)The latest rows, newest first
Parameters
-
$limitint, default50: how many -
$capabilitystring|null, defaultnull: only this capability
Source: framework/core/Audit.php:46
Caligrafy: Describe it. Approve it. Ship it. Your idea turned into a complete app: every feature built once, for web pages, Vue apps, a REST API and AI agents, and online.
Why Caligrafy: the benefits, the measurements, and what it is not yet
-
Quickstart with Claude: describe it, approve it, ship it
or the Quickstart: the same three steps by hand, in 5 minutes
- How Caligrafy works: the ideas in 5 minutes
- Your first capability: build a feature, page included
-
Capabilities in depth
-
#[Capability]: every option - Scheduled jobs: capabilities on a timer
- Notifications: web push and email
- Background jobs: capabilities run later
- Migrations: database changes, in order
- The cache
-
Testing your app
- Dependency injection
- Validation rules
-
Web pages
- Views and page components
- Design styles: another look, one setting
- Declarative routes: every request, in one line
-
Route::page·Route::form·Route::auth·Route::webhook·Route::download·Route::redirect·Route::action·Route::snap· capability routes - How to export a list as CSV
- Controllers: moving one to a declared route · how to, step by step
- Forms and helpers
-
Vue apps
-
Mobile and tablet
-
REST API
-
AI agents
-
Snap Cards and the assistant
- Payments: Stripe and crypto
- Face detection and face login
- OpenAI: chat and images · Assistants
- Open models: Llama, Mistral...
- Chatbots with Watson
- Machine learning in the browser
- Metadata and social cards · search and analytics
-
Putting it online: the production image, the database at every start
-
API reference: every class and method
-
Routing ·
Route·AuthRoutes· pages ·Controller· capabilities -
Data ·
Model·Query· sign-in · jobs ·Notify· payments · AI -
Interfaces · Snap ·
Snap\Cards·Snap\Conversation· testing · tooling · helper functions
-
Routing ·
-
Caligrafer: every command
- Project · build · data · agents · jobs and tests · maintenance · deploy
- Installation, configuration and Caligrafer
- Routing · Request · Controllers · Views
- Vue: flow of information · routes · validations
- Security and upgrading
- Release notes: 2.26 · 2.25 · 2.24 · 2.23 · 2.22 · 2.21 · 2.20 · 2.19 · 2.18 · 2.17 · 2.16 · 2.15 · 2.14 · 2.13 · 2.12 · 2.11 · 2.10 · 2.9 · 2.8 · 2.7 · 2.6 · 2.5 · 2.4 · 2.3 · 2.1 · October 2026 · changelog
- Testing Caligrafy (contributors)