Skip to content

Docuccino vs Scribe

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:

  • Annotate vs infer. Scribe extracts route and validation basics automatically and asks you to annotate the rest — responses, query parameters not covered by validation, field descriptions. Docuccino inverts this: an embedded static-analysis engine infers the contract from your code (resources, exception handlers, query builders several calls deep, auth middleware), and attributes or docblocks are targeted overrides.
  • Executes your code vs never executes it. Scribe’s flagship response mechanism runs your endpoints — inside a rolled-back transaction, with .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.

  • Real captured responses from the generator itself. Scribe invokes your endpoint (transaction-wrapped, .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.
  • Factory-driven examples. @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.
  • Closure rules. Scribe’s 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.
  • Fractal / league transformers (@transformer*, custom serializers). Docuccino has no Fractal integration; you’d write a TypeToSchema extension.
  • Publishable views and custom example-request languages. There are no Docuccino views to publish and edit; theming the page means writing a viewer driver that renders it — full control, but you start from a blank page rather than from a theme. Docuccino also has no per-language example-request setting: the snippet languages are whichever the driver offers. (Standalone prose like Scribe’s intro.md / auth.md is covered — Docuccino compiles a folder of Markdown guides into the document; see Adding your own pages.)
  • Hand-written custom endpoints (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.
  • The .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.
  • Localization of the generated UI via Laravel lang files. Docuccino’s viewer has no UI-string localization.
  • Try-It-Out wired to your auth, including Sanctum CSRF. Docuccino has a try-it console through the Scalar viewer, but no equivalent auth-config block.
  • Maturity and community — years of production use across four Laravel majors, and the troubleshooting knowledge that comes with it.

You don’t have to switch in one go — the two write to different places, so run both during a transition:

Terminal window
# Scribe stays untouched; generate Docuccino's document alongside it
php artisan docuccino:export --out=docs/openapi.docuccino.json

Then migrate incrementally, per controller or group:

  1. Move config — routes, info, base_url → servers, auth.* → security + Sanctum/Passport, docs_url/middleware → the viewer, --config → a second document.
  2. Delete inferred annotations — @bodyParam / @queryParam / @urlParam / @apiResource* / @responseField / @authenticated. Regenerate and docuccino:diff after each pass to confirm nothing regressed — the diff is your safety net.
  3. Translate the survivors — @group → #[Group], @response → #[Response], Example: → #[Example], @hideFromAPIDocumentation → #[ExcludeFromDocs].
  4. Handle the hard parts — response-call-derived examples (save representative payloads as JSON files and point #[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).
  5. Decommission Scribe — remove knuckleswtf/scribe, config/scribe.php, the .scribe/ folder, published Scribe views, and any lang/scribe.php.