laravel-workersv0.4.10
documentation

docs / Compatibility

Compatibility

Two tables, both generated from the repository: 105 Laravel documentation pages and the 49-row feature map behind them. A construct without a verdict is a gap, and gaps are build-blocking.

Reading the verdicts

verdictmeaning
runs on the edgeworks as-is, inside the budget
runs with declared gapsworks with a documented trade
needs a padanan (port)a platform wall: write the equivalent
not applicable at the edgelocal tooling or a meta page

For your own app the same words come from the build: READINESS: RUNS ON THE EDGE,RUNS WITH DECLARED GAPS (declared gaps ship, and are named), orDOES NOT RUN (a route has no decision). The table below is about Laravel's own documentation surface, not about a particular app.

Laravel documentation, page by page

15 runs59 gaps5 port26 n/a105 pages
pageverdictwhat it means for your app
ain/adev tooling (Boost/AI guidelines)
ai-sdkgapsHTTP + streaming via fetch fits
artisann/alocal-only; edge CLI equivalents
authenticationgapskernel SessionGuard oracle-verified (M14); wired M15 to M20 (auth.test 4/4, login-flow E2E)
authorizationgapsthe Gate facade compiles: Gate::allows()/denies() answer from the compiled policy (the guest rules included); authorize()/any()/define()/before() fail loud
billinggapsCashier via fetch; webhooks = HMAC routes (padanan)
bladegapsviews compile (echo/if/loops/expr); the layouts compile too (M40: the sections travel to the layout, @extends/@section/@yield); the Blade components (<x-...>) stay a port
boostn/adev tooling only
broadcastingportSSE/polling padanan; WS via DO/external
cachegapsthe Cache facade + the cache() helper compile to the D1 database store; the tags need a taggable store (LW002, the D1 store has none, Laravel throws there too); a non-database driver fails the build (it would read and write a different store)
cashier-paddlegapssame as billing (padanan)
collectionsgapskernel shim subset; fail loud per method
concurrencygapsPromise.all; 6-conn limit → batch
configurationrunsfrozen at build + secrets
console-testsn/alocal; edge = parity harness
containergapscompile-time DI subset
contextgapsrequest-scoped shim
contractsrunstype-erased; two-sided custom drivers
contributionsn/ameta
controllersgapsM10/M17/M22/M23/M28: GET/POST/PUT/PATCH/DELETE actions compiled (one handler per verb); \->validate() + flash (M19); edirect()->intended() + session regen/invalidate (M20); FormRequests, Cache/Hash/Storage facades, class constants and Eloquent writes compile (M23/M28); the route-model binding compiles (M40: show(Post $post) fetches the model from the segment); pagination + Eloquent relations still a port; fail-loud LW002 (unsupported construct) / LW003; REQUEST_CALL synced to kernel (#2: date removed, str added)
csrfrunssession token; @csrf/@method
databasegapsD1 builder: where/or/join/group/having
database-testinggapssqlite↔D1 parity harness
deploymentrunsbuild → wrangler deploy (D1 + assets)
documentationn/ameta (TOC page)
duskn/alocal browser tests
eloquentgapsschema-driven ORM subset
eloquent-collectionsgapscollections shim
eloquent-factoriesgapslocal dev tooling; no runtime code (unverified)
eloquent-mutatorsgapscompiled accessors/casts; build-time fillable dump; hashed cast
eloquent-relationshipsgapslazy + eager subset
eloquent-resourcesgapscompiled toJSON subset
eloquent-serializationgapstoArray/toJson subset
encryptiongapsAES-CBC+HMAC WebCrypto; serialize subset
envoyn/alocal (SSH deploys)
errorsgapsbuild-time compiled error views when present; an unhandled exception renders that 500 (JSON requests get Laravel's {"message":"Server Error"}); status-only stubs otherwise; new <PHP exception> + throw_if/throw_unless compile (M34)
eventsgapsEvent::dispatch(new X(...)) compiles (M39c: the build compiles the event classes + their listeners, dist/src/classes.generated.mjs, and the dispatch calls them synchronously, like Laravel); a dispatch of an event name fails loud (no class, no listener); a queued listener fails loud (the edge has no queue consumer) → port; a listener that injects a service compiles (M44: the edge builds the app services the build compiled and adapts the framework services, kernel/service-adapters.mjs: the mailer, the logger, the worker hands the live one down); a service the build does not compile fails loud (the edge has no container and no file for it)
facadesgapskernel shims; the Cache/Hash/Storage facades compile (M23/M28/M30); the Gate facade compiles (M39a: the policy is compiled at build time); mail/queue/notification/password still unmapped → port
filesystemgapswired (M28/M30): Storage:: → R2 when bound, else the D1 blob store (lw_blobs); the default disk compiles (Storage::disk('local') is that same store, M39b), a named disk needs a bucket of its own (LW002: R2 asks for a credit card); file_exists/public_path stay build-only (no local FS); the upload compiles (M40 part 5): the multipart body is parsed at the edge, the file object + store()/storeAs() answer Laravel's values (the file rule and the size rules in kilobytes work), the content type of a file (finfo) fails loud. Tiers: free plan first (D1 blob store, 1,5 MB/objek, tanpa kartu) → R2 kalau app butuh (disk bernama / objek besar; R2 minta kartu kredit) → paid plan hanya kalau free path memang tak cukup (keputusan operator; tak ada construct yang diblokir karena free plan kecil, build hanya berhenti kalau edge tak bisa menjawab seperti PHP)
folion/aNOT ported (0 code); Laravel Folio page routing unsupported
fortifygapscompile routes/actions, or manual auth (padanan)
frontendrunsVite unchanged; proven
hashinggapsPBKDF2 both-sides; bcrypt runs on free (bcryptjs, verdict safe); argon (N/A at edge); PHP 8.3 password_get_info parity + hashed cast
headgapsmetadata pkg; view-time render
helpersgapskernel shim subset incl. html-entities, php-json, php-serialize, php-bytes (PHP strings as byte-strings: strlen/substr/ord semantics), php-json-ordered (the json_decode reader every PHP-facing decode uses, rt.json_decode, Collection::fromJson, a json cast, the request body: document key order via __lwOrder, exact 64-bit ints when a double carries the literal else LW006); abort_if/abort_unless/now/today compile (M33)
homesteadn/alocal VM
horizonn/aRedis dashboard; padanan CF dashboard
http-clientrunsfetch (already HTTP)
http-testsn/alocal; parity harness covers
imagesportno GD/Imagick → CF Images or build-time
installationrunstoolchain (§7)
licensen/ameta
lifecycleruns§3 execution model
localizationgapsfrozen JSON; pluralization subset; the compiled rung answers the dates in UTC (the Workers' timezone) while Laravel uses config('app.timezone'), an app with a non-UTC timezone gets different dates at the edge (a documented divergence; the plan: M57)
logginggapsthe Log:: facade compiles (M33) → the platform log (the Workers console); no Log module (a local file sink)
mailgapswired (M28): the worker builds the mailer from the app config (mail.default -> log/noop/resend) and hands it to the handler; the fluent Mail::to()->send() compiles (M57: the mailable is a class, the build compiles it and the kernel's Mailable base carries Laravel's fluent surface) → port; padanan = HTTP provider (SMTP impossible, hard)
mcpgapsHTTP/SSE servers compile; stdio (N/A at edge) (padanan)
middlewaregapscompiled pipeline wired (throttle + guest/auth stages); aliases auth.session/precognitive/can* fail loud
migrationsrunsops-time; remote via wrangler/SQL
mixn/alegacy; Vite is the path
mockingn/alocal; harness mocks fetch
mongodbn/aD1 = SQLite only; explicit out-of-scope
notificationsgapsno module; needs mail+queue+sse padanan (mail/db channels; broadcast → SSE)
octanen/aserver runner irrelevant at edge
packagesn/ameta (this repo becomes one)
paginationrunskernel paginator
passportgapsOAuth endpoints compile; JWT WebCrypto (padanan)
passwordsgapstokens + mail; signed URLs (signed URLs ok; token+mail wiring pending)
pennantgapsD1/KV store driver (padanan)
pintn/alocal style tool
precognitiongapsvalidation kernel + headers
processesportno process spawning → off-edge padanan
promptsn/aCLI-only
providersgapscompile-time registration subset
pulsegapsD1-backed; or CF analytics padanan
queriesgapsbuilder: where/or/join/group/having/distinct
queuesgapswired (M28): the worker hands the queue adapter to the handler (a port can push); the Queue::push(SomeJob::class) facade fails loud, the job would never run at the edge: the worker has no queue handler, so nothing consumes the queue binding, and the binding carries a JSON body, not a PHP job (the runtime throws the same). The paths that work: a Cloudflare Queues binding with a consumer you write (a limited version is on the Workers free plan; the paid plan charges per operation), or a D1 table the cron trigger reads (a port handler writes the row, the edge has no producer for it). The job class is not compiled either → port; CF Queues (a limited version on the Workers free plan; the paid plan charges per operation) / D1 queue padanan
rate-limitinggapsD1 counters (KV driver pending); throttle build-gated
readmen/ameta
redisportKV/D1 drivers; Upstash REST optional
releasesn/ameta; pin 13.x policy
requestsgapskernel request subset
responsesgapskernel responses + SSE
reverbportWS server → DO/external/SSE padanan
routingrunscompiled router
sailn/alocal Docker
sanctumgapstokens; SPA via sessions (padanan)
schedulinggapswired (M29): the build dumps the app schedule into dist/src/schedule.generated.mjs + dist/cron.json (the Cloudflare trigger list); the worker's scheduled handler queues the due events for the app runtime (the edge triggers, it does not run artisan)
scoutgapsengines via HTTP; D1 FTS fallback (padanan)
searchgapsFTS5; vector → CF Vectorize padanan
seedinggapslocal dev tooling; no runtime code (unverified)
sessiongapsD1 store; write budget soft; driver kv/encrypt pending
socialitegapsfetch-based OAuth (padanan)
starter-kitsgapskit = app code; covered with auth + blade (padanan)
stringsgapsStr shim subset; fail loud
structureruns§7 source of truth
telescopen/alocal debugger
testingrunslocal PHPUnit + edge parity harness
upgraden/ameta; upgrade = recompile + parity
urlsrunsurl/route/asset; signed URLs
valetn/alocal env
validationgapsthe full rule surface (KNOWN_RULES + the conditional/presence sets); $request->validate() → redirect-back / 422 JSON (M19); unique/exists via D1; $attributes display names + wildcard custom messages (T10); a FormRequest compiles its rules()/messages()/attributes() and its prepareForValidation() hook (the compiled class extends the kernel's kernel/form-request.mjs, the request surface the hook uses); the timezone rule tests PHP's own identifier list (kernel/timezone-ids.data.mjs, generated by tools/timezone-ids.php; Intl's list is a different set); the messages are Laravel 13's own (kernel/validation-messages.mjs + the generated kernel/validation-messages.data.mjs, the app's published lang/ file wins, the build dumps it); a rule object (Rule::file()->max()) and a FormRequest that overrides prepareForValidation() fail loud (the compiled rung evaluates the string rules only); the rule parameters are parsed like Laravel (CSV - in:"a","b" works, nothing is trimmed) (M61a)
verificationgapssigned URLs + mail (signed URLs ok; mail wiring pending)
viewsgapscompiled view handlers (echo/if/concat subset)
viterunsmanifest shims; proven today

The feature map

The coverage index above is the reader-facing view of this map: 49 rows across 7 areas, each stating what Laravel does, what the edge does instead, and the trade where there is one.

29 runs17 gaps2 port1 n/a

A. Routing & HTTP

Laravelon the edgeverdictnote
Basic routes, paramsCompiled router (pattern match)runsfrom route:list dump
Named routes, route(), url()Frozen name mapruns
Route groups / middlewareCompiled pipelinerunshandleHttp runs web group + auth/guest/throttle; 11-step oracle parity (M15)
RedirectsResponse redirectrunsredirect()/->to/->route + ->intended() (M20, pulls url.intended); back() needs session
Signed URLsHMAC in kernelrunssame key derivation
Route model bindingCompiled lookupgapsid-first subset initially
Custom middlewareCompiled like controllersruns
Throttle middlewareCounter in D1/KVrunsnumeric throttle:n[,m] wired in handleHttp over the D1-backed database cache store (M18); named throttle:name needs its AppServiceProvider definition → build fails loud
CSRFToken in session storerunskernel PreventRequestForgery wired in handleHttp + end-to-end oracle parity (M15)
Cookie encryptionLaravel Encrypter parity (AES-CBC + HMAC) via WebCryptorunsEncryptCookies wired in handleHttp; round-trip + prefix parity in cookies.test.mjs

B. Views

Laravelon the edgeverdictnote
Echo, @if/@foreach/@include/@extends/@section...Compiled JS render fnsruns
@vite / @fontsRuntime shim reading build manifestrunsalready proven in snapshots
@php blocksRestricted subsetgapselse build error (no silent)
Blade components (x-..., slots)Phase 2gapspadanan now: inline partial
Custom directivesRegistered renderersgapslater

C. App code

Laravelon the edgeverdictnote
Controllers / closuresPHP-subset → JSrunsmulti-verb controllers compiled (GET/POST/PUT/PATCH/DELETE, one handler per verb): scalar params + Request $request injection, model chains + views, session()/old()/csrf_token()/csrf_field()/request()/cookie()/auth()/Auth::, response()/response()->json(), Cache::/Hash::* facades, class constants (RouteServiceProvider::HOME, dumped at build time), Eloquent writes (create/fill/save/delete, $model->attr = ...); fail loud elsewhere
Service classes / helpersPHP-subset → JSruns
Form RequestsCompile rules + authorizerunsrules()/messages()/attributes() are dumped at build time (tools/formrequest-dump.php) and compiled into the handler: the request is validated before the method body (like the container resolving the FormRequest), and validated() returns the ruled attributes. authorize() must return true, prepareForValidation() is not supported → build fails loud. A class-typed parameter that is not a FormRequest is a service → build fails loud
ValidationRule subset in kernel; unique/exists via D1gaps$request->validate()/validateWithBag() compile to an awaited kernel-validator call; a failure renders like Laravel, 302 back (Referer → previous URL → /) with flashed errors + _old_input (minus dontFlash), or 422 JSON for expectsJson. $attributes display names (exact/wildcard/PHP-truthiness/raw-implicit/Str::snake fallback) and :attribute custom-message replacement are wired; json session serialization only (the ViewErrorBag php-serialization path fails loud) (M19/T10). A date-comparison parameter (before/after/date_equals) reads Carbon's relative grammar, weekday, month name, noon/3pm, +3 days/2 weeks ago, next week, first day of january, verified 0/405 divergent vs PHP 8.3 (audit #671)
Events / ListenersSynchronous compiledrunsqueued listeners → queue
Exceptions / error pagesKernel map → static error pagesgapsresources/views/errors/{code}.blade.php compile at build time and render for abort()/HTTP errors and for an unhandled exception (500; its expectsJson answer is Laravel's {"message":"Server Error"}) when present; otherwise status-only stubs

D. Data

Laravelon the edgeverdictnote
MigrationsPHP-side, applied to remote D1 (SQL export / wrangler)runsops-time, not request-time
Seeders / factoriesLocal + SQL importruns
Eloquent CRUDGenerated model metadata + D1gapssubset: find/findOrFail/all/where/whereIn/orderBy/limit + writes (create, fill, save, delete, forceDelete, restore, $model->attr = …); the write path needs the D1 binding (rt.db())
Relationshipsincludes / eager subsetgapsbelongsTo, hasMany first
Casts / accessorsCompiledgaps
Query builder subsetKernel builder → SQLgaps
Raw SQLPass-through to D1runs
TransactionsD1 atomic batch()gapsinteractive tx unsupported (platform) → padanan: single-request batch
PaginationLength-aware paginatorruns
SearchD1 (SQL LIKE / FTS5); vector → CF Vectorize padananruns

E. State & security

Laravelon the edgeverdictnote
SessionsD1-backed store + encrypted cookierunskernel Store + cookie/D1 handlers, oracle byte-parity (M11); wired in handleHttp, payload bytes compared per step (M15); regenerate()/invalidate()/regenerateToken() wired from compiled controllers (M20)
Auth (guard, login/logout, remember)Kernel + D1 usersrunskernel SessionGuard, EloquentUserProvider, BcryptHasher, auth/guest middleware oracle-verified (M14); wired in the compiled pipeline + end-to-end parity (M15); the auth.basic middleware is honoured (Basic header → guard->attempt(), 401 + WWW-Authenticate: Basic realm="Basic" on failure); a guard driver the app registers ITSELF (Auth::extend('seal', fn), a closure the rung cannot run) is answered by the app's own JS through a guard port (registry.json guards → ports/<slug>.mjs → PortedGuard, kernel/auth.mjs): the driver is the app's, the middleware/auth()->user()/401 answer around it stay compiled, user() runs at most once per guard per request, and a ported method the port does not export fails loud. An UNPORTED non-session driver refuses the build (it would otherwise answer for an unauthenticated request)
Password hashingbcrypt via bcryptjs ($2y$ parity)runskernel BcryptHasher make/check/needsRehash/verifyConfiguration (M5); free-plan CPU banked (measured); the Hash facade compiles to it (Hash::make/check/needsRehash/isHashed) with the app's hashing.bcrypt.rounds
Rate limitingD1 counters (cache table)runskernel RateLimiter + ThrottleRequests oracle-verified (M12); runs on the D1 database cache store (M18). increment is read-then-write, D1 has no interactive transactions

F. Platform services

Laravelon the edgeverdictnote
MailHTTP providers (Resend etc.) via fetchgapskernel/mail.mjs exists (log/noop/resend); wired (M28), the worker builds the mailer from the app config (mail.default → log/noop/resend) and hands it to the handler; the fluent Mail::to()->send() compiles (M57: a mailable is a class - the build compiles it and the kernel's Mailable base carries Laravel's fluent surface; the mailer sends the recipients, the subject and the rendered view) → the methods the kernel mailer does not implement (raw, html, queue, later) fail loud (padanan = HTTP provider; SMTP impossible, hard)
Queues / JobsCloudflare Queues (a limited version on the Workers free plan; the paid plan charges per operation - 10,000 operations/day included on the free plan) or D1 queuegapswired (M28), the worker hands the queue adapter to the handler (a port can push); the Queue::push(SomeJob::class) facade fails loud, the job would never run at the edge: the worker has no queue handler, so nothing consumes the queue binding, and the binding carries a JSON body, not a PHP job (the runtime throws the same). A compiled job class is the supported form: Job::dispatch() writes the D1 lw_jobs row and the cron trigger drains it (M63), so the job runs after the response; the build refuses the push without a cron trigger, and refuses a push whose job class did not compile (the drain has no handler for it). The job classes compile (M63) → port
SchedulerCron Triggers → one dispatcher reads schedulegapswired (M29): the build dumps the app schedule (cron expression + command) into dist/src/schedule.generated.mjs + dist/cron.json (the Cloudflare trigger list); the worker's scheduled handler queues the due events for the app runtime (the edge triggers, it does not run artisan), delivery needs the app's QUEUE producer binding (without it the event is dropped and the worker, the /__lw/cron answer and the build all say so); the trigger also drains the D1 job queue, which needs no binding. The HTTP face of the trigger (POST /__lw/cron) is closed by default: without LW_CRON_SECRET it answers 404 (indistinguishable from a missing route), with a secret the caller must present it in X-LW-Cron
CacheD1 database store (Laravel parity) + KV cache-asiderunsDatabaseCacheStore mirrors Illuminate\Cache\DatabaseStore 13.37, oracle-verified incl. raw rows (M18); KV stays for build-time/simple caching (eventual consistency trade); the Cache facade compiles to it (get/has/put/add/forever/forget/remember/pull/increment/decrement/touch/flush)
Storage / filesystemUploads → R2 or the D1 blob store; file_exists/public_path → build-time snapshotgapsno local FS (hard) → object storage is the padanan; wired (M28): the Storage:: facade compiles to the store (put/get/exists/delete) and the worker hands the adapter to the handler; M30: R2 when the app binds it, else the D1 blob store (one row per object in lw_blobs, no extra Cloudflare product, no card); with neither binding the build fails loud and names the missing one (no silent in-memory writes). Shadow-verified: the compiled side writes to the D1 blob store and reads it back byte-identically to the real app on the local disk. M40 part 5: the upload compiles, the multipart body is parsed at the edge, $request->file() answers Laravel's file object, $file->store()/storeAs() write to the app's default disk (the same store) and answer the disk-relative path; the file rule and the size rules (in kilobytes) work; the content type of a file (finfo) fails loud
HTTP clientfetchrunsno raw sockets (hard); 6 concurrent conns → batch/queue
LoggingConsole + optional external sink via fetchgapsno Log module; Log:: facade not mapped → port; console + fetch sinks only
Realtime / WebSocketsPolling / SSE; Durable Objects beyondportwall: no long-lived conn → padanan: polling/SSE first
i18nLang frozen to JSON; pluralization subsetgapsLang frozen to JSON; pluralization subset (see localization)

G. Frontend / DX

Laravelon the edgeverdictnote
Vite buildUnchangedrunsalready working
ArtisanLocal only; edge CLI (build/ship)runs
PHPUnitLocal unchanged; parity suite addedruns
Livewire / Inertia / FilamentOut of v1; Inertia feasible (JSON port) phase 3; Livewire hardportdocumented, not promised
Telescope / Horizon / DebugbarLocal-only toolsn/aN/A at edge

The construct census

The documentation map answers “which Laravel features”. The census answers “which PHP constructs”: 118 audited, 47 compile, 64 fail loud and 7 are documented at build level. Each row carries the batch it was proven in.

The census, row by row ·where the refusals cluster ·how the audit was extracted

Source: docs/READINESS.md, MAP.md §5 — extracted from repository arfoux/laravel-workers at 1b5a50ac8 (v0.4.9)