Body & query parameters
@bodyParam / @queryParam covered by your validation rules, plus query parameters read
straight out of your Spatie Query Builder calls.
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 is purely static: no database, no side effects, no seed data — but
also no runtime-captured payloads.| 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 |
| Example responses | Captured from real calls and factories | Inferred, or authored with #[Example] |
| Postman collection | Yes | No — import the OpenAPI file |
| 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) |
| Custom HTML theming | Yes, Blade templates you publish | No — Scalar is configurable, not a template layer |
| Generated-UI localization | Yes, via Laravel lang files | No |
| Documented intermediate format | — | Yes (the UIR) |
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 78 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 | ✓ | — |
| RFC 9457 Problem Details preset | ✓ | — |
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 | ✓ | — |
| A documented intermediate format (the UIR) | ✓ | — |
| Postman collection export | — | ✓ |
| 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.
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 calls.
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, not just the action body. |
@bodyParam / #[BodyParam] |
Delete — inferred; #[BodyParameter] patches one property |
FormRequests, 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] on an action pins the success response’s example body; #[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:
Scribe (config/scribe.php) |
Docuccino (config/docuccino.php) |
|---|---|
routes.match.prefixes: ['api/*'] |
documents.default.routes.include: ['api/*'] |
routes.match.domains / routes.exclude |
documents.default.routes.closure / 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) |
groups.default |
documents.default.tags.default_strategy (controller | none) |
--config scribe_admin (multi-docs) |
A second named entry under documents |
postman / openapi output toggles |
OpenAPI is always emitted; docuccino:export picks the format |
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. Docuccino never runs your code, so its examples are inferred or authored.@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: 78
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 Guides, pages & prose.)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].#[Response] / #[Example]), .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 (import the OpenAPI file).knuckleswtf/scribe, config/scribe.php, the .scribe/ folder,
published Scribe views, and any lang/scribe.php.