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 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.

  • Real captured responses. 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. Docuccino never runs your code, so its examples are inferred or authored.
  • 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.
  • A native Postman collection. Docuccino emits OpenAPI; import that file into Postman instead.
  • 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: 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.
  • Fractal / league transformers (@transformer*, custom serializers). Docuccino has no Fractal integration; you’d write a TypeToSchema extension.
  • Fully Blade-customizable HTML — publishable views, custom themes, custom example-request languages. Docuccino’s viewer is Scalar: configurable, but not a template layer you own. (Standalone prose like Scribe’s intro.md / auth.md is covered — Docuccino compiles a folder of Markdown guides into the document; see Guides, pages & prose.)
  • 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_urlservers, 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 (paste representative payloads into #[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).
  5. Decommission Scribe — remove knuckleswtf/scribe, config/scribe.php, the .scribe/ folder, published Scribe views, and any lang/scribe.php.