Body & query parameters
@bodyParam / @queryParam covered by your validation rules, plus query parameters read
straight out of your Spatie Query Builder allow-lists — wherever in your code they are built.
Scribe is a mature, well-loved documentation generator with a clear mission: generate API documentation for humans. It produces a polished HTML page with human-friendly prose, code samples in several languages, and an in-browser tester — and it’s been doing it well across four Laravel major versions. If your priority is a beautiful hand-tunable HTML doc, Scribe is excellent.
Docuccino takes a different path: it infers your API contract from your code with static analysis, emits an OpenAPI document as its primary artifact, and treats annotations as overrides rather than the source. This page compares the two fairly, credits Scribe plainly where it’s stronger today, and shows how to move across if you decide to.
The tools differ at the root, and it colors everything else:
.env.docs isolation and before/after hooks — to
capture real payloads. Docuccino’s generator is purely static: no database, no side effects, no seed
data. It gets real payloads a different way, from responses your test suite already produced and
you committed — so the capture happens where execution already lived, and the doc build still only
ever reads a file.| Scribe | Docuccino | |
|---|---|---|
| Primary artifact | A human-first HTML page | An OpenAPI document, plus a bundled viewer |
| OpenAPI export | A secondary output, off by default (OpenAPI 3.0) | The point: 3.2, 3.1 or 3.0, JSON or YAML, with reusable components/schemas |
| Authoring model | Annotate, with partial inference | Infer from code; attributes are overrides |
| Executes your application code | Yes, for response calls | Never — static analysis only |
| Production install | Ships with its extraction stack | Adapter only — the analysis engine is a dev dependency |
| Query params from query builders | Annotate them | Inferred, traced several calls deep, including into the method that builds each allow-list entry |
| Example responses | Captured from real calls and factories | Inferred, authored with #[Example], or recorded from your test suite |
| Postman collection | Yes | Yes — a postman export target, emitted from the same build |
| Try-it-out console | Yes, in its own theme | Yes, via the bundled Scalar viewer |
| Byte-stable output you can commit | — | Yes |
| Semantic diff + CI version gate | — | Yes (docuccino:diff --enforce) |
| Contract testing from your own suite | — | Yes — assertions over the generated document |
| Custom HTML theming | Yes, Blade templates you publish | Yes, by writing a viewer driver — the whole page, not a published template |
| Generated-UI localization | Yes, via Laravel lang files | No |
| Documented extension with a published schema | — | Yes (the Docuccino extension) |
Capability by capability. The two tools optimize for different artifacts, so neither column wins
everywhere: Docuccino leads on schema and contract work, Scribe on the rendered page and on examples
taken from a running app. ✓ means built-in support; — means not supported.
| Capability | Docuccino | Scribe |
|---|---|---|
| Validation rules → parameter documentation | ✓ | ✓ |
Form requests and inline validate() |
✓ | ✓ |
Rules → OpenAPI schema constraints (pattern, bounds, format) |
✓ | — |
| Form requests read without instantiating them | ✓ | — |
| Custom rule classes documented from the rule class | ✓ | ✓ |
| Spatie Data objects | ✓ | — |
| Spatie Query Builder parameters | ✓ | — |
| Laravel Actions | ✓ | — |
Both tools read the same rules, but they get them differently: Scribe instantiates your form request
and calls rules(), which resolves rules assembled at runtime and needs the method to be safe to call;
Docuccino reads the rule list statically and maps 80 named Laravel rules onto schema constraints, so
starts_with:INV lands as a pattern and between:1,5 as minimum/maximum in the schema, rather
than as prose beside the parameter.
| Capability | Docuccino | Scribe |
|---|---|---|
| API Resources | ✓ | ✓ |
| Pagination envelopes | ✓ | ✓ |
Eloquent model schemas from casts and @property |
✓ | — |
| Backed enums, with case descriptions | ✓ | — |
Reusable components/schemas, deduped and $ref’d |
✓ | — |
| Path parameter types from route-model binding | ✓ | — |
Polymorphic MorphTo → discriminated oneOf |
✓ | — |
| JSON:API resources | ✓ | — |
| Fractal / league transformers | — | ✓ |
| Examples captured from a real call to your endpoint | — | ✓ |
| Examples built from your factories, with states and relations | — | ✓ |
The split is the philosophies showing through. Scribe documents a rendered instance of your response
and is very good at it; Docuccino documents the response’s shape, which is what a $ref-able
component schema, a client generator and a semantic diff all need.
| Capability | Docuccino | Scribe |
|---|---|---|
| Marking one endpoint authenticated or not | ✓ | ✓ |
| Security requirements derived from route middleware | ✓ | — |
| Sanctum in both token and stateful-cookie modes | ✓ | — |
Sanctum token abilities (x-abilities) |
✓ | — |
| Passport OAuth2 scopes, per operation | ✓ | — |
| Passport client-credentials (machine-to-machine) | ✓ | — |
Role / permission requirements (x-permissions) |
✓ | — |
Rate-limit 429 responses + Retry-After / X-RateLimit-* headers |
✓ | — |
| Try-it-out console wired to your auth, including Sanctum CSRF | — | ✓ |
Scribe’s auth is a deliberate one-time declaration: describe the scheme once in auth.*, then mark the
exceptions with @authenticated / @unauthenticated. It’s less to get wrong, and for many APIs it’s
the whole story. Docuccino derives the requirement from the middleware stack per guard driver instead,
so a new protected route is documented as protected the moment it’s routed.
| Capability | Docuccino | Scribe |
|---|---|---|
| Hand-written error responses | ✓ | ✓ |
Framework defaults (422 / 401 / 403 / 404) |
✓ | — |
| Errors implied by middleware, bindings and validation | ✓ | — |
| Your real error shapes, read from your exception handling | ✓ | — |
In Scribe an error response is something you write down — @response 404 {...} — and it’s exactly as
right as you keep it. Docuccino reads your render callbacks, exception render() methods and
Responsable exceptions and documents the shape they actually produce, then fills in the framework’s
own shapes for the rest. See Error responses.
| Capability | Docuccino | Scribe |
|---|---|---|
| In-browser try-it console | ✓ | ✓ |
| Markdown prose pages alongside the reference | ✓ | ✓ |
| Corrections that survive a regenerate | ✓ | ✓ |
| OpenAPI as the primary artifact, always emitted | ✓ | — |
| Reusable component schemas in the OpenAPI output | ✓ | — |
| Deterministic, byte-stable output | ✓ | — |
| A spec artifact you commit and review in a pull request | ✓ | — |
| Semantic diff + CI gate on breaking changes | ✓ | — |
| Assertions holding your test suite’s traffic to the spec | ✓ | — |
| A report of documented endpoints your suite never exercises | ✓ | — |
| Documented examples validated against their own schemas | ✓ | — |
| A documented extension with a published schema (the Docuccino extension) | ✓ | — |
| Single-page HTML doc with Blade templates you own | — | ✓ |
| Localized generated UI | — | ✓ |
| Hand-written endpoints for routes you have no code for | — | ✓ |
Both tools let you correct what was extracted and keep the correction: Scribe by hand-editing the
.scribe YAML, Docuccino with an attribute at the source or a standard
OpenAPI Overlay. Where Scribe is
stronger covers the bottom four rows in detail.
The three contract-testing rows are the other side of never running your endpoints. Docuccino does not run your endpoints to capture a response the way Scribe does — but your test suite already runs them, so it holds that traffic to the generated document instead, reports which documented endpoints the suite never touched, and can record those responses into committed files the build publishes as examples. See contract testing.
Because Docuccino infers where Scribe asks you to annotate, a lot of Scribe’s annotation surface simply disappears on migration:
Body & query parameters
@bodyParam / @queryParam covered by your validation rules, plus query parameters read
straight out of your Spatie Query Builder allow-lists — wherever in your code they are built.
Response fields & resources
@responseField, @apiResource* become typed, described fields straight from your API Resources,
Data objects, and model schemas.
URL parameters
@urlParam covered by route-model binding and primary-key types.
Auth markers
@authenticated covered by middleware detection (Sanctum dual-mode, Passport scopes).
Most of Scribe’s annotations either map directly or are deleted because Docuccino infers them. The biggest win is deletion.
| Scribe (tag / attribute) | Docuccino | Notes |
|---|---|---|
Docblock title/description, #[Endpoint] |
Kept as-is | Docblock summary/description are read natively. |
@group / #[Group] |
#[Group] |
Direct swap; repeatable; controller-level. |
@subgroup / #[Subgroup] |
#[Group] + tags.definitions parent |
Tag the operation with the subgroup name, then declare the subgroup tag with its parent. Hierarchy is OAS 3.2; a 3.1 export flattens it with a warning. |
@authenticated / #[Authenticated] |
Delete — inferred | From auth middleware (Sanctum, Passport). |
@unauthenticated / #[Unauthenticated] |
#[Unauthenticated] |
Clears the inferred requirement. |
@hideFromAPIDocumentation |
#[ExcludeFromDocs] |
Also #[Internal] and #[InDocs] for finer control. |
@header / #[Header] |
#[HeaderParameter] |
Direct swap. |
@urlParam / #[UrlParam] |
Delete — inferred; #[PathParameter] to add a format |
Route-model binding gives the type. |
@queryParam / #[QueryParam] |
Delete — inferred; #[QueryParameter] to add one |
Query builders are traced through your helper methods, into the body of the method that builds each allow-list entry, and into a builder subclass’s constructor — not just the action body. |
@bodyParam / #[BodyParam] |
Delete — inferred; #[BodyParameter] patches one property |
Form requests, inline validate(), Spatie Data. |
Validation-rule comments (// … Example: x) |
#[BodyParameter] for the ones worth keeping |
The rules themselves are read; the prose beside them isn’t. Where a field needs a description or example, patch that one property. (A comment on a Spatie Query Builder allowedFilters entry is read — see Query Builder.) |
@response / #[Response] |
#[Response] |
Patches field-level, keeping inferred siblings. |
@responseField / #[ResponseField] |
Delete — inferred | Field descriptions come from the source property/docblock. |
@apiResource* / #[ResponseFromApiResource] |
Delete — inferred | Resource + pagination inference, no factory needed. |
@transformer* (Fractal) |
No Fractal integration | Would need a TypeToSchema extension — see where Scribe is stronger. |
@responseFile / #[ResponseFromFile] |
Inline via #[Response] / #[Example] |
No file-loading equivalent. |
Example: / No-example markers |
#[Example] / #[IgnoreParam] |
#[Example] pins example payloads on a response, the request body or a parameter — one, or several named ones, written inline or loaded from a JSON/YAML file; #[IgnoreParam] drops a parameter you’d rather not document. |
examples.faker_seed |
Delete — unnecessary | Output is deterministic by construction. |
groups.order config |
tags.definitions (weight-ordered) + tags.map |
Tag objects are sorted by ascending weight, then name. |
inheritedDocsOverrides() |
Not needed for route context | Path params, bindings, and controller-level attributes resolve against the concrete child route; use attributes or overlays for anything an inherited method’s body inference doesn’t cover. |
Custom rule docs() method |
#[RuleSchema] on the rule class |
Same idea, different ergonomics: Scribe calls a method on your rule, Docuccino reads an attribute on it — one declaration, every field that uses the rule. Its fields map onto the rule vocabulary (type, enum, pattern, min/max, format, description, example), so anything the vocabulary can’t express is still a RuleTransformer. |
.scribe/** hand-edits, custom.*.yaml |
OpenAPI Overlays or attributes at source | Different philosophy — see below. |
| Strategy plugin API | OperationExtension, TypeToSchema, ExceptionToResponse, RuleTransformer, DocumentTransformer |
Register from any provider, any order. |
| — | #[DeprecatedOperation] |
New on migration: marks an operation (or a whole controller) deprecated, with a reason. |
| — | #[HiddenFromRequest] |
New on migration: drops a server-populated Data-class property from the documented request body. |
Scribe’s flat config/scribe.php maps onto Docuccino’s per-document config. Docuccino reads two
files: docuccino.yaml at your project root for everything that shapes a document, and
config/docuccino.php for the viewer, which Laravel registers on every boot.
Scribe (config/scribe.php) |
Docuccino (docuccino.yaml, unless noted) |
|---|---|
routes.match.prefixes: ['api/*'] |
documents.default.routes.include: ['api/*'] |
routes.match.domains / routes.exclude |
documents.default.routes.filter / routes.exclude |
title / description / intro_text |
documents.default.info.title / .description |
base_url |
documents.default.servers |
auth.* |
documents.default.security + Sanctum/Passport integrations (inferred) |
type: laravel, laravel.docs_url, laravel.middleware |
documents.default.viewer (route, gate, middleware, source) — in config/docuccino.php |
groups.default |
documents.default.tags.default_strategy (controller | none) |
--config scribe_admin (multi-docs) |
A second named entry under documents |
postman / openapi output toggles |
export.targets — list both formats and one build writes both |
routes.match.domains maps onto more than it asks for. Scribe matches domains against patterns;
routes.filter is a class implementing RouteFilter,
handed each route’s domain along with its URI, name, action and middleware — so a domain match is
one return statement, and a rule that needs to consult something (a tenant registry, a feature
flag) takes it as a constructor dependency, because the container builds the class. It is also why
the key names a class rather than taking an inline predicate: the setting lives in docuccino.yaml,
and a configuration file has no form for a closure.
See the configuration reference for every option.
Scribe is mature and does several things Docuccino doesn’t. If you rely on any of these, weigh them carefully before you switch — for some teams they’re the whole reason to stay.
.env.docs isolated) and shows the genuine JSON your API returns, including
anything computed at runtime that static analysis can’t see — and it does that for every documented
endpoint, whether or not you have tests. Docuccino can publish genuine payloads too, but only ones
your test suite produced and you committed
(recorded examples), so
an endpoint no test exercises still gets an inferred or authored example. If your HTTP coverage is
thin, Scribe reaches further here.@apiResourceModel states=… with=… builds examples from your real
factories, with states, eager-loaded relations and pivot data. Docuccino has no equivalent — it
documents the response’s shape, not a rendered instance of it.docs() method on a custom Rule object now has a direct counterpart —
#[RuleSchema] on the rule class, read
wherever the rule object appears — and Docuccino maps the whole string-rule vocabulary besides: 80
named Laravel rules, from alpha and starts_with patterns to const for accepted/declined, date
comparisons, numeric bounds, not_in and distinct, plus Rule::enum (including ->only() /
->except()), Rule::in, Rule::exists and Rule::unique. A RuleTransformer teaches it any rule
you invent. What neither tool reads is a closure rule or a Rule::when() conditional — there’s no
docs() to call on a closure — so both fall back to something you write by hand: a comment beside the
rule in Scribe (lighter, and its advantage here), a
#[BodyParameter] in Docuccino, which also names the
field in a validation.rule-unrecoverable diagnostic rather than quietly omitting it.@transformer*, custom serializers). Docuccino has no Fractal
integration; you’d write a TypeToSchema extension.intro.md / auth.md is covered — Docuccino compiles a folder of Markdown guides
into the document; see Adding your own pages.)custom.*.yaml) — an escape hatch for documenting anything at
all, including third-party package routes (Passport, Fortify) you have no code access to. In
Docuccino that’s an OpenAPI Overlay or a DocumentTransformer — see
customizing the output..scribe hand-edit merge workflow — patch any extracted fact in YAML and have the patch
survive regeneration. Docuccino keeps corrections in source (attributes) or in standard
OpenAPI Overlays.You don’t have to switch in one go — the two write to different places, so run both during a transition:
# Scribe stays untouched; generate Docuccino's document alongside itphp artisan docuccino:export --out=docs/openapi.docuccino.json# Turn on Scribe's OpenAPI export, generate both, and read them side by side# to see where each tool covers your API differentlyphp artisan scribe:generateThen migrate incrementally, per controller or group:
base_url → servers, auth.* → security + Sanctum/Passport,
docs_url/middleware → the viewer, --config → a second document.@bodyParam / @queryParam / @urlParam / @apiResource* /
@responseField / @authenticated. Regenerate and docuccino:diff after each pass to confirm
nothing regressed — the diff is your safety net.@group → #[Group], @response → #[Response], Example: →
#[Example], @hideFromAPIDocumentation → #[ExcludeFromDocs].#[Example(file: …)] at them), .scribe hand-edits and custom.*.yaml (re-home as attributes or
Overlays), Fractal transformers (a TypeToSchema extension), custom Blade themes (not carried
over), Postman consumers (add a postman export target).knuckleswtf/scribe, config/scribe.php, the .scribe/ folder,
published Scribe views, and any lang/scribe.php.