Commit the output
Deterministic bytes mean a clean PR diff every time. The committed spec is your source of truth.
The default configuration rebuilds your document on every request — perfect for local development, wrong for production. This guide covers the production story: generate in CI, commit the artifact, serve it without re-analyzing, gate who can see it, and let CI catch breaking changes.
Generate in CI and commit the artifact. Docuccino’s output is deterministic, so a committed
docs/openapi.json is a reviewable, diffable record of your API.
php artisan docuccino:export --fail-on=warningServe the committed file, not a fresh build. Point the viewer at the artifact so no request ever triggers analysis:
// config/docuccino.php → documents.default.viewer'source' => 'artifact', // re-emits the committed export.path; never re-analyzesGate the viewer route so only the right people reach it (below).
Diff on every change to enforce your versioning policy in CI (below).
Generation is safe to run in any environment, including a CI container with no services attached: the pipeline reads types, so it makes no database queries, sends no mail, and dispatches no jobs.
Because the analysis lives in docuccino/inference-phpstan and you install it as a dev dependency,
composer install --no-dev deploys the adapter alone: no PHPStan, no Larastan, nothing that reads
your source at runtime. The production dependency chain is docuccino/laravel → docuccino/core →
a JSON-Schema validator, a YAML parser and two small parsing libraries. Serving from artifact or
cache then costs one file read.
That is also why the export belongs in CI: the machine that has your dev dependencies is the one that can analyse your code. A production box that only serves the committed document never needs to.
This guide assumes your app serves the viewer. If it doesn’t — because you read the docs only in development, or you publish the spec to an external host — install the adapter as a dev dependency too and production ships nothing of Docuccino at all. Everything below about generating and diffing in CI still applies; only the viewer sections stop mattering. See the installation scenarios.
viewer.source decides where the served spec comes from. In production, serve pre-built bytes:
source |
Where the spec comes from | Use it when |
|---|---|---|
generate |
Rebuilt on every request | Local development, or a gated internal viewer on a small app |
artifact |
The committed export.path, re-emitted |
You commit the spec — the usual production answer |
cache |
The docuccino:cache-warmed payload |
High-traffic or public viewers; you’d rather warm on deploy than commit |
The viewer guide has the rest of the viewer’s options.
For cache, warm it on deploy and clear it when you regenerate:
php artisan docuccino:cache # build and store each document's payloadphp artisan docuccino:clear # forget the cached payloadSet the backing store with cache.store. A cold cache falls back to generating the spec and logs a
warning, so a forgotten warm-up degrades loudly instead of serving nothing. See the
commands reference.
By default the viewer is reachable only in your local environment. To expose it anywhere else, name a
viewer.gate ability and define it in a service provider — the gate guards the HTML and .json
routes, and you should keep throttle in viewer.middleware when the spec endpoint is public. The
viewer guide walks through it step by step.
To turn the runtime viewer off for one document, set its viewer.route to null — export still
works. To turn Docuccino off entirely in an environment, use the master switch:
DOCUCCINO_ENABLED=falseWith enabled false, no viewer routes are registered at all and every command except
docuccino:clear aborts with a non-zero exit — so a production box can ship the package without
serving or generating anything.
Regenerate whenever the code that shapes your API changes — new routes, changed validation, new
resources or error handling. The natural home for that is a pull-request job: export, then diff the
fresh document against the committed one and hold the changeset to your
versioning policy.
It’s the same job when your docs live on an external host: export with --out= to wherever your
upload step reads from, and add that upload after the checks below pass.
# 1. Regenerate; fail if anything got worse than a warning.php artisan docuccino:export --fail-on=warning
# 2. Structural check against the UIR schema.php artisan docuccino:validate
# 3. Compare with the committed artifact and enforce the versioning policy.php artisan docuccino:diff docs/openapi.json --against=HEAD --enforcename: API docson: pull_request
jobs: spec: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.4' - run: composer install --no-interaction --prefer-dist - run: cp .env.example .env && php artisan key:generate
# Diff BEFORE overwriting: --against=HEAD reads the committed artifact from git. - run: php artisan docuccino:diff docs/openapi.json --against=HEAD --enforce - run: php artisan docuccino:export --fail-on=warning - run: php artisan docuccino:validate
# The committed artifact must match a fresh build — output is deterministic. - run: git diff --exit-code docs/openapi.json--against=HEAD reads the old side out of git (git show HEAD:docs/openapi.json), so the job works
on a clean checkout without a second build. The final git diff --exit-code is the step that keeps
contributors honest: because identical code produces identical bytes, a stale committed spec fails
the build.
An OpenAPI artifact is the friendliest thing to review, and it’s what most tooling wants. Commit a
UIR document instead (--format=uir) when you also want stable identities and provenance in the
repo — a richer diff, at the cost of a noisier file. Either way, pair it with --provenance=none if
you’d rather source line numbers didn’t churn in your PRs.
If your API is served per tenant on a subdomain, describe it once with a server variable rather than a server per tenant:
// config/docuccino.php → documents.default.servers'servers' => [ ['url' => 'https://{tenant}.example.com', 'variables' => [ 'tenant' => ['default' => 'acme', 'description' => 'Tenant slug'], ]],],The viewer renders the variable as an editable field, and the value flows into try-it-out requests.
Server variables are emitted verbatim — see servers in
the configuration reference.
Commit the output
Deterministic bytes mean a clean PR diff every time. The committed spec is your source of truth.
Gate then cache
Serve artifact or a warmed cache behind a gate — never generate on a hot public route.