The honest, feature-by-feature mapping between Laravel 13.x and Suprnova. Use this when you're asking "does Suprnova have X?" and want a yes/no/where answer in one row.
Sections mirror the Laravel docs index so a Laravel developer can scan top-to-bottom. Within each section the columns are always the same:
| Laravel | Suprnova | Status | Notes / link |
|---|
The Status column uses four values:
| Symbol | Meaning |
|---|---|
| shipped | Same surface, same behaviour (often same method names) |
| diverged | Same job, different shape because Rust makes a better choice possible |
| not yet | Genuinely planned, not yet on disk |
| by design no | Won't ship - explanation in the Notes column |
The relevant chapter (where one exists) is linked from the Notes column.
This is a live map. Suprnova ships every Laravel 13.x surface across the 30 documented domains; the gaps listed below are the real, current gaps as of the shipped framework.
Architecture concepts
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Request Lifecycle | Application → Server → handle_request chain |
shipped | Lifecycle |
| Service Container | Container + App facade, three-layer (task / thread / global) |
diverged | Task-local for per-request, thread-local for tests - Container |
Contextual binding (when()->needs()->give()) |
No contextual bindings - one binding per trait per container layer | by design no | The container is TypeId-keyed with no runtime reflection to key a binding on "who is asking". Compose explicitly: pass the dependency in, or bind a distinct newtype per consumer. Container |
| Service Providers | bootstrap() function + #[service], #[policy], #[command], observer macros |
diverged | No registration class - bootstrap is one function; macros use inventory for compile-time registration. Bootstrap |
| Facades | Static App::get, Cache::*, Mail::*, Auth::*, Storage::*, Queue::*, Bus::*, Event::*, Notification::*, Gate::*, Schedule::*, DB::*, Vector::* |
shipped | Same call shape; the facades are real types, not aliases |
| Contracts | Traits - Mailer, KeyValueStore, Hasher, Channel, VectorDriver, Evaluator, PaymentProvider, etc. |
shipped | All public seams live on traits; bind by trait, swap implementations freely |
Getting started
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Installation | cargo install --git …suprnova-cli then suprnova new <name> |
shipped | Installation |
| Configuration | Typed config via #[derive(Config)] + Config::register |
diverged | Compile-time typed instead of array bags. Configuration |
| Agentic Development (AI) | No first-class AI SDK in framework | by design no | Use the crates you'd use anyway (async-openai, anthropic-rs, tokenizers, etc.) under App::bind(Arc<dyn YourLlm>) |
| Directory Structure | src/{actions,bootstrap,controllers,middleware,models,routes} |
shipped | Same intent, Rust-idiomatic layout. Structure |
| Frontend | Inertia v3 over Svelte 5 / React 19 / Vue 3.5 | shipped | Frontend, Pages, TS Types |
| Starter Kits | Nebula (auth) and Pulsar (full product site), plus the plain suprnova new scaffold |
shipped | Two kits ship today - Nebula is the Breeze equivalent; Pulsar adds docs, blog, community, and RBAC. Starter Kits |
| Deployment | Single binary; Docker / Railway / DO / Hetzner recipes | diverged | One artifact, not a PHP runtime + opcache + FPM. Deployment |
The basics
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Route definitions | routes! macro + get! / post! / put! / patch! / delete! / any! / head! / options! / fallback! / ws! |
shipped | Routing |
| Route parameters | {id} path params + req.param("id") |
shipped | Optional params via {id?}; constraints via where!() |
| Route names | .name("posts.show") on the route + url("posts.show", &[("id", "42")]) |
shipped | URL Generation |
| Route groups | group! macro with .prefix() / .middleware() / .name() / .controller() |
shipped | Group middleware is flattened onto each route at register time |
| Resource routes | resource!("posts", PostController) registers the 7 standard routes |
shipped | apiResource!, only(...), except(...) all supported |
| Signed URLs | sign_url(...), sign_route(...), verify_signature(...) |
shipped | HMAC-SHA256 with APP_KEY |
| Route model binding | #[handler] extracts Post from {post} via RouteBinding impl |
shipped | AutoRouteBinding derive auto-implements for #[suprnova::model] types |
| Rate limiting | throttle:60,1 middleware + RateLimiter::for_signature |
shipped | Rate Limiting |
| Middleware | impl Middleware trait; register globally or per-route |
shipped | Middleware |
| Middleware groups + aliases | register_middleware_group, register_middleware_alias |
shipped | Look up by string name in routes |
| CSRF Protection | CsrfMiddleware + csrf_token() / csrf_field() / csrf_meta_tag() |
shipped | Per-session token validation is the default. Optional SameOriginOnly, AllowSameSite, and OriginOnly policies consult Sec-Fetch-Site; origin enforcement is not enabled by default. CSRF |
| Controllers | #[handler] pub async fn show(req: Request) -> Response |
shipped | Controllers are modules of free functions, not classes. Controllers |
| Single-action controllers | A handler is already a single function; group into modules | shipped | The Rust convention - no __invoke ceremony |
| Requests | Request struct with .input(), .param(), .query(), .header(), .cookie(), .json(), .file(), etc. |
shipped | Requests |
| Form Requests | #[derive(Data, Validate, FormRequest)] |
shipped | Validation runs as you extract |
| File uploads | req.file("avatar")? returns UploadedFile; streaming multipart with size + part caps |
shipped | Auto-spill to tempfile above threshold |
| Responses | HttpResponse builders + json_response!() / text_response!() / Redirect::to / Inertia responses |
shipped | Responses |
Streamed responses (eventStream, stream, streamJson) |
HttpResponse::sse(...) / event_stream(...) / stream_bytes(...) / stream_json(...) |
shipped | Same wire shapes @laravel/stream-{react,vue,svelte}'s hooks expect. SSE |
withoutCookie / withoutCookies |
.without_cookie(name) / .without_cookies([...]) on HttpResponse, Response, Redirect, RedirectRouteBuilder |
shipped | Cookie::forget_with(name, path, domain) for a cookie that wasn't set at / |
| Views (Blade) | Server-rendered Inertia pages (Svelte/React/Vue) - no Blade equivalent | diverged | Inertia is the view layer. Use Pages instead of Blade |
| Asset Bundling (Vite) | Vite 8 ships in every scaffold; suprnova serve runs Vite + backend together |
shipped | Manifest reading + HMR auto-wired |
Static assets (public/, served by the web server in Laravel) |
StaticFiles::public() in-process fallback handler serving public/ at the web root |
shipped | StaticFiles::from_dir(...) + cache_control(...); no separate web server needed |
| URL Generation | url("posts.show", &[…]), route("posts.show", …), redirect(...), redirect_to(...) |
shipped | URL Generation |
| Session | session(), session_mut(), flash bag via req.flash() |
shipped | Database-backed by default via DatabaseSessionDriver; the encrypted browser cookie carries the session identifier and activity-touch metadata, not the session data bag. Session |
Cookie queue (Cookie::queue) |
Cookie::queue/queued/unqueue/expire - a task-local jar SessionMiddleware drains onto the response |
shipped | Requires SessionMiddleware in the chain; queued by name, not name+path like Laravel's CookieJar |
| Validation | #[derive(Validate)] + 35 built-in rules + Rule/ValueRule/AsyncRule traits |
shipped | Url uses Laravel's scheme allowlist and Url::protocols([...]) mirrors url:http,https. Async rules (e.g. Unique) hit the DB. ArrayKeys/Distinct are ValueRules over serde_json::Value, matching Laravel's array:keys and distinct. InArray takes the other field's list directly instead of Laravel's in_array:other.* rule string, and Contains/DoesntContain match JSON string elements exactly. Gt/Gte/Lt/Lte take an explicit CompareWith operand (literal number, numeric sibling, or sibling compared by character count) instead of inferring Laravel's four size measures; array and file comparisons have no counterpart. Validation |
Password rule (Password::defaults(), uncompromised()) |
Password::min(n) + strength builders (.letters(), .mixed_case(), .numbers(), .symbols()) + .uncompromised() |
shipped | Have I Been Pwned k-anonymity check; fails open on a network error, matching Laravel's NotPwnedVerifier. Validation |
| Error Handling | FrameworkError, AppError, HttpError trait, panic boundary in execute_chain_safely |
shipped | Error Handling, Error Model |
| Logging | tracing subscriber with structured fields, LogFormat (json / pretty / compact) |
diverged | One log line is a JSON document; request_id always present. Logging |
Log channels / file drivers (single, daily, monthly, stack) |
tracing writes structured lines to stdout; the platform rotates and ships them |
by design no | Containers, systemd, and every log shipper already do rotation and retention. Re-implementing it in-process duplicates the platform and hides logs from it. Logging |
| Abort helpers | abort_if(cond, status, msg), abort_unless(...), abort_with(status, msg) |
shipped | Same shape as Laravel's abort_if family |
Digging deeper
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Artisan Console | Per-app console binary built from #[command] + #[derive(Command)] |
shipped | Console. cargo run --bin console <subcommand> |
| Tinker (REPL) | No REPL | by design no | Write a one-off cargo run --bin xxx script or a #[suprnova_test] |
| Broadcasting | BroadcastHub + Channel / PrivateChannel / PresenceChannel + Broadcastable |
shipped | sea-streamer fanout for multi-node. Broadcasting |
| Cache | Cache::get/put/forget/remember/rememberForever/increment/... + InMemoryCache, RedisCache |
shipped | Atomic ops + tagged cache + cache locks (LockGuard). Cache |
| Collections | eloquent::Collection<M> with Laravel-shape methods |
shipped | Deref<Target = Vec<M>> so existing Vec idioms still work. Collections |
| Concurrency | Tokio everywhere - tokio::spawn, tokio::join!, tokio::select! |
shipped | The whole framework is async. The Laravel Concurrency::run([...]) facade doesn't ship; Tokio is the answer |
| Context | Context::put / Context::get / ContextStore + auto-injection into queue / mail / events |
shipped | Context |
| Contracts | All public seams are traits | shipped | See the "Architecture / Contracts" row above |
| Events | EventFacade::dispatch(e).await?, #[derive(Event)], EventDispatcher, queued listeners, subscribers |
shipped | Events |
| File Storage | Storage::disk("local"|"s3"|"azblob"|"gcs"|"memory") over OpenDAL |
shipped | Same put/get/delete/copy/move/exists/url surface. Path-traversal protection built in. On a local disk, put/write, the streaming writer, and copy are all staged under <root>/.suprnova-atomic/ and published in one step, and a conditional write is published with link(2) so it stays a true exclusive create; append is the one in-place operation. Laravel's local driver writes straight to the target, where a partial length is observable and a crash truncates the live object. Storage::register_read_through composes two disks into a read-through disk that promotes fallback hits onto the primary, with copy: false to skip promotion and fallback-spanning copy / rename. Filesystem |
| Helpers | Equivalents are in their home modules (no kitchen-sink helpers.md) |
diverged | E.g. URL helpers live in urls.md, string helpers in std/heck, array helpers in std::collections - Rust does this with crates, not a global namespace |
| HTTP Client | Http::get/post/... builder + Http::fake(...) for tests |
shipped | Auto-records requests; assert_sent / assert_not_sent; .retry_when(predicate) narrows the built-in retry policy with a RetryContext. HTTP Client |
Image (Illuminate\Image) |
Image::from_bytes/from_path/from_disk/from_upload/from_stream + the same operation and terminal surface |
shipped | Lives in suprnova::media. Two drivers like Laravel's gd/imagick: IMAGE_DRIVER=oxideav (default, pure Rust) or magick. Reads and writes PNG, JPEG, WebP, GIF, BMP; AVIF output is deferred pending the in-house AV1 encoder publish. Header-checked decode limits. Images |
| HEIC decoding in the default driver | IMAGE_DRIVER=magick on a host with the libheif delegate |
by design no | HEVC is patent-encumbered and the only credible pure-Rust decoder is dual AGPL/commercial, so no built-in decoder ships. Same shape as Laravel, where GD cannot read HEIC at all and Imagick needs the delegate compiled into both the binary and the PHP extension. Images |
| Localization | Lang::get / get_with / try_get / has + the __!("key", name: value) macro over Fluent .ftl catalogs in lang/<locale>/, LocaleMiddleware detection, translated validation messages, ICU4X formatting |
shipped | The same catalog is served to the browser at /_suprnova/lang/<locale>.ftl and typed by generate-types. Localization |
Mail::to(...).send(MyMail { ... }).await? + drivers smtp/ses/mailgun/postmark/sendgrid/resend/log/memory/file |
shipped | Mailable trait + Tera-rendered HTML/text bodies; SES sends carry TenantName / ConfigurationSetName / ListManagementOptions; queued dispatch routes via .on_queue(...) / .on_connection(...), outranking Queue::route. Mail |
|
| Notifications | Notify::send(&user, notif).await? + channels mail/database/broadcast/webpush |
shipped | Notifiable trait + Notification per channel; queued dispatch (Notify::queue) carries per-notification queue/timeout/fail_on_timeout/max_tries/backoff onto each channel's job via the same EnvelopeOverrides primitive Mail uses. Notifications, Web Push |
| Package Development | Workspace adapter crates (e.g. suprnova-payments-stripe) |
shipped | Same shape as Laravel packages: depend on the framework, bind into the container, expose macros if needed |
| Processes (running shell commands) | tokio::process::Command from the stdlib |
by design no | No facade - Tokio's API is already the right shape |
| Queues | Queue::push(job).await? + drivers sync/memory/database/redis/null, batches, chains, JobMiddleware, FailedJobStore |
shipped | Queues |
| Job-declared delay | fn delay() -> Option<Duration> on Job, honored by Queue::push and Queue::bulk |
shipped | An explicit Queue::push_later / Queue::later(delay, job) call always wins over the job's own default. Queues |
Queue::forward |
Queue::forward(from, to) / Queue::forward_on(from, to, connection), applied to the envelope and to the worker's --queue list |
shipped | Queue-to-queue only: connection gates the redirect rather than selecting a driver, and is compared against the process connection name so the push and the worker's claim gate on the same value; to is required where Laravel's is optional. Queues |
| Unique-job skipped event | queue::events::UniqueJobSkipped { job_name, unique_id, connection } |
shipped | Fired on the push side when push_unique dedupes; the call still returns Ok(false) |
Queue pausing (queue:pause / queue:resume) |
Queue::pause/resume/pause_all/resume_all/is_paused/paused_queues, cache-backed, with QueuePaused / QueueResumed / QueuesPaused / QueuesResumed events |
shipped | A per-queue pause only takes effect on a worker started with an explicit --queue=... list; resume_all doesn't clear a per-queue pause. A running worker also emits WorkerQueuePaused / WorkerQueueResumed once per transition and queue:work prints a line for each; their queue field is Option<String>, because a worker started without --queue has no queue names to report under a global pause. Queues |
| Retrying transient Redis commands | Read-shaped Redis commands retry once on a connection-level failure, each attempt awaiting the driver's reconnect budget; REDIS_COMMAND_RETRIES adds more |
shipped | Laravel's command_retries covers every command through one dispatch point and a 60-entry allowlist; Suprnova retries per call site, and no setting makes a write or a queue pop retry. Cache |
After-commit dispatch (afterCommit()) |
fn after_commit() -> bool on Job, EnvelopeOverrides::after_commit per push, Queue::push_after_commit |
shipped | The whole push waits for the commit, events included, and a rollback discards it; a deferred push_unique still takes its lock immediately so dedupe works inside the transaction. Manual DB::begin_transaction never defers. Queues |
| Failover queue connection | FailoverQueueDriver over an ordered connection list, via QUEUE_DRIVER=failover + QUEUE_FAILOVER_CONNECTIONS |
shipped | Writes fall through the list; pop, the counters and the listings stay on the first connection, so each fallback needs its own worker. QueueFailedOver is edge-triggered, and bulk_push falls through per envelope so each keeps its own delay. Queues |
ShouldBeUniqueUntilProcessing |
fn unique_until_processing() -> bool on Job, released after the middleware pass and before the handler |
shipped | Owner-scoped release, so a redelivered attempt never releases a newer dispatch's lock. A job a middleware releases back onto the queue keeps its lock. Queues |
Debounced jobs (#[DebounceFor]) |
Job::debounce_for / max_debounce_wait / debounce_id, plus Queue::push_debounced(job, DebounceOptions) |
shipped | Trait methods rather than a class attribute, so a typo is a compile error. Every dispatch is enqueued and the collapse is settled at the worker; a job declaring both debounce_for and unique_id is refused with a FrameworkError where Laravel throws. Chains and batches refuse a debounced job outright. Queues |
| Debounced queued listeners | Job::debounce_for on the listener's job, or DebouncedListener::new(window, build).keyed_by(...) |
shipped | Laravel puts the attribute on the listener class; Suprnova's listener-to-job bridge already runs through Queue::push, so declaring it on the job covers the common case and DebouncedListener covers a per-registration window. Events |
Queue inspection (pendingJobs / delayedJobs / reservedJobs) |
Queue::pending_jobs(queue) / delayed_jobs / reserved_jobs, Option<&str> collapsing Laravel's all*Jobs() twin into one call |
shipped | InspectedJob DTO (id/queue/name/attempts/payload/created_at); the trait default is an honest Err rather than an empty collection; sync/null override with Ok(vec![]); Redis's reserved_jobs is per-consumer. Unlike Laravel these do not follow a Queue::forward, so they report the literal queue you name - which is how a backlog left behind on a forwarded queue stays visible. Queues |
| Schedule per-task timezone | .timezone(chrono_tz::Tz) / .try_timezone("name") per task, Schedule::timezone default, schedule:list --timezone |
shipped | Typed chrono_tz::Tz instead of Laravel's string; the schedule-wide default is Schedule::timezone in schedule::register rather than an app.schedule_timezone config key, and an unpinned task keeps the process-local zone. Scheduling |
| Rate Limiting | RateLimiter::for_signature(...), ThrottleRequestsMiddleware, RateLimitMiddleware |
shipped | Sliding window via SlidingWindowConfig. Rate Limiting |
| Search (Scout) | No first-party full-text search adapter | not yet | Vector search ships today via Vector; keyword-search Scout-equivalent is planned |
| Strings (helpers) | heck crate (case conversions), std::str, regex |
diverged | Same crates the rest of the Rust ecosystem uses; no Str::camel($x) global |
| Task Scheduling | Schedule::call/command/task + #[derive(Task)] + cron syntax + schedule:run worker |
shipped | Scheduling |
| Idempotency keys | Idempotency::remember(key, ttl, body) - Stripe-style replay protection |
shipped | Caller namespaces the key with the route + user / business identity. Idempotency |
| Request timeout | TimeoutMiddleware configurable per route |
shipped | Rust-native - abort the in-flight future, free the worker. Timeout |
| Feature Flags (Pennant) | Feature + Evaluator + FeatureMiddleware + admin CRUD |
shipped | Sub-second propagation via FeatureSync trait. Feature Flags |
| Observability (Pulse) | OpenTelemetry via init_telemetry, Metrics, tracing everywhere |
diverged | OTel is the lingua franca for Rust observability - point your collector at the binary. Observability |
| Telescope (debug dashboard) | No equivalent yet | not yet | Deferred to v2+; the framework's tracing + OTel output covers most diagnostic needs |
| Pulse (perf dashboard) | No equivalent yet | not yet | Same as Telescope - surface metrics with your existing observability stack until a dashboard ships |
| Vector search | Vector::driver("memory"|"qdrant"|"pinecone"|"mariadb") |
shipped | No "Postgres pgvector only" gatekeeping. Vector Search |
Suprnova-exclusive (no Laravel equivalent)
| Suprnova | What it is | Notes / link |
|---|---|---|
ws!() macro + WebSocket handlers |
Typed WS routes that share the router + middleware stack | WebSockets |
| Workflows | Long-running stateful work with retries, sleep, step boundaries | Workflows |
| Supervisors | Supervisor trait with panic-catch auto-restart for long-lived tokio tasks |
Supervisors |
| Web Push (VAPID) | Browser push notifications as a first-class channel | Web Push |
| Multi-connection read/write split | READ_REPLICA_CONNECTION_NAME + DB::on("read").select(...) |
Database |
| HTTP/2 + WebSocket on the same socket | hyper.with_upgrades() in Server::run |
Lifecycle |
| Markdown content + docs pipeline | MarkdownRenderer (sanitised comrak → syntect → ammonia) + build_docs(DocsBuildConfig) → searchable DocsCatalog of DocsChapters |
Heading extraction + slugify_heading; powers Markdown docs / blog with no separate static-site generator |
Security
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Authentication | Auth::user/check/login/logout/attempt, Authenticatable trait, Guard per name |
shipped | Authentication |
| Multiple guards | Guard registered by name (web, api, …) via AuthManager |
shipped | SessionGuard, TokenGuard, custom impls |
| User providers | EloquentUserProvider<U>, DatabaseUserProvider, custom via UserProvider trait |
shipped | Auth Flows |
| Email Verification | EmailVerification + EnsureEmailVerifiedMiddleware + EmailVerificationMail; MustVerifyEmail contract |
shipped | Provider-backed and actor-bound - Auth flows |
| Password Reset | PasswordReset + Magnetar first-email-proof transaction or verified UserProvider fallback + reset/change mail |
shipped | Magnetar handles atomic first proof; provider-backed apps can reset already verified users - Auth flows |
| Brute-force throttling | Magnetar lockout engine + BruteForce + LoginThrottleMiddleware |
shipped | Account lockout plus framework IP/route limiting |
| Two-Factor (TOTP) | Framework TwoFactor compatibility facade plus Magnetar factor engine |
shipped | Recovery codes, replay protection, and factor-gated integrated sign-in |
| Remember-me | Magnetar purpose-bound rotating credential behind the framework cookie | shipped | Auth-epoch checks, rotation, anomaly handling, and legacy fallback |
| OAuth (Socialite) | Magnetar provider registry and Auth::oauth(provider) facade |
shipped | OAuth, Apple form_post, PKCE/state binding, verified identity policy - OAuth |
| Sanctum (API tokens) | BearerTokenMiddleware over Magnetar bearer sessions |
diverged | Authenticates bearer sessions; no separate Sanctum token-management API |
| Passport (OAuth server) | Magnetar protocol and plugin engines | diverged | Engine primitives ship; no Laravel Passport-compatible application facade |
| Fortify (auth backend) | Framework Auth/auth_flows facades over Magnetar engines |
shipped | Framework owns HTTP, mail, events, cookies, and application binding |
| Authorization (Policies / Gates) | Gate::allows/denies + #[policy] impl PostPolicy + Authorizable trait + macro registration + Gate::default_denial_response |
shipped | Authorization |
| Roles & permissions (spatie/laravel-permission) | HasRoles trait + roles / permissions / role_has_permissions tables (CreateRbacTables) + RoleMiddleware / PermissionMiddleware (fail-closed) |
shipped | First-party, not a community package. create_role / give_permission_to_role / assign_role_to_model helpers; layers on top of Gate/Policy. Authorization |
| Encryption | Crypt::encrypt/decrypt + CryptPurpose AAD binding |
shipped | AES-256-GCM, key rotation via APP_KEY_PREVIOUS. Encryption |
| Hashing | hash::* + BcryptHasher, Argon2idHasher, Argon2iHasher, needs_rehash, is_hashed, verify |
shipped | Bcrypt default; argon2id available. Hashing |
Database
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| DB::table('users')->where(...)->get() | DB::table("users").db_where("id", "=", 1).get().await? |
shipped | Database, Queries |
| Multiple connections | DB::on("read") + ConnectionRegistry |
shipped | Read/write split first-class |
| Transactions | DB::transaction(|tx| async move { ... }).await? |
shipped | Savepoints + retry-on-deadlock |
| Query events | QueryListener + QueryExecuted event |
shipped | DB::listen(|q| { ... }) |
| Raw expressions | DB::raw("..."), DB::select("...", &[...]) |
shipped | Parameter binding required (no string interpolation) |
| Postgres / MySQL / SQLite | All three first-class via SeaORM | shipped | URL detection in database::config::database_type() |
Postgres keepalives_* DSN options |
DB_IDLE_TIMEOUT / DB_MAX_LIFETIME / DB_ACQUIRE_TIMEOUT / DB_TEST_BEFORE_ACQUIRE / DB_PING_AFTER_IDLE pool liveness |
diverged | sqlx exposes no TCP keepalive setter, so Suprnova recycles and pings pooled connections instead. Database |
| MariaDB | First-class as its own option (vector + JSON + temporal) | diverged | Treated separately because of multi-paradigm features Laravel ships as Postgres-only |
| Redis | Used by drivers (cache/queue/rate-limit) - no separate Redis::* facade |
diverged | Reach for redis crate directly when you need ad-hoc commands; cache/queue/rate-limit cover 95% of typical use |
| MongoDB | No first-party adapter yet | not yet | Use mongodb crate directly via App::bind |
| Query Builder | Builder<M> with db_where / or_where / where_in / where_between / where_null / where_has / with / with_count / order_by / group_by / having / paginate / etc. |
shipped | Queries |
whereBinary() family |
Builder::where_binary / or_where_binary / where_not_binary / or_where_not_binary, and DB::table(...).where_binary(...) |
shipped | MySQL and MariaDB emit = binary; Postgres and SQLite return an error instead of a collation-dependent match. Queries |
| Pagination | LengthAwarePaginator, Paginator (simple), CursorPaginator |
shipped | All three serialise to Laravel-shape JSON. Pagination |
| Migrations | #[derive(DeriveMigrationName)] struct M; + up/down + Migrator |
shipped | Run via suprnova migrate/migrate:rollback/migrate:status/migrate:fresh. Migrations, CLI Migrations |
| Seeders | Seeder trait + db:seed subcommand |
shipped | Per-model factories. Seeding |
Eloquent ORM
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
class User extends Model |
#[suprnova::model(table = "users")] struct User { ... } |
shipped | The struct IS the SeaORM Model. Eloquent |
| Find / first / get | User::find(id), User::query().first(), User::all(), Builder::get |
shipped | All async |
| Create / update / delete | User::create(attrs), user.update(attrs), user.delete() |
shipped | attrs! { name: "...", email: "..." } macro for partial attrs |
| Mass assignment guards | #[model(fillable = [...])] / #[model(guarded = [...])] + unguarded || { ... } scope |
shipped | prevent_silently_discarding_attributes() for strict mode |
| Soft deletes | #[model(soft_deletes)] auto-injects deleted_at + SoftDeletes trait |
shipped | with_trashed(), only_trashed(), restore(), force_delete() |
| Prunable / MassPrunable | #[prunable] impl Prunable for User { ... } + model:prune worker |
shipped | Cascade-pinned to relations |
| Timestamps | Auto created_at/updated_at if columns are present |
shipped | Disable via #[model(timestamps = false)] |
| Primary key types | i64 default; UUID / ULID via #[model(unique_id = "uuid")] or unique_id = "ulid" |
shipped | Auto-generates id on insert |
| Local scopes | #[scopes(User)] impl User { fn active(b: &mut Builder<User>) { ... } } |
shipped | Method dispatch on Builder<M> |
| Global scopes | impl GlobalScope for ActiveOnly { ... } + register |
shipped | Stripped via Builder::without_global_scope |
| Relationships (11 kinds) | HasOne, HasMany, BelongsTo, BelongsToMany, HasOneThrough, HasManyThrough, MorphOne, MorphMany, MorphTo, MorphToMany, MorphedByMany |
shipped | Per-family morph enum. Relationships |
wherePivot family (incl. the closure form) |
where_pivot / where_pivot_op / where_pivot_in / where_pivot_not_in / where_pivot_null / where_pivot_not_null / where_pivot_between / where_pivot_not_between / where_pivot_group plus or_ twins |
diverged | Reads only - a pivot filter never narrows attach / detach / sync, and eager loads do not carry it. Relationships |
| Eager loading | User::query().with(&["posts", "posts.comments"]).get() |
shipped | EagerLoadDispatch is sealed; only macro-generated relations can implement it |
| Lazy loading prevention | prevent_silently_discarding_attributes(true) |
shipped | Same shape as Laravel's preventLazyLoading |
| Aggregates on relations | with_count("posts"), with_sum("orders", "total"), with_avg, with_min, with_max |
shipped | Single subquery per aggregate |
whereHas / whereDoesntHave |
where_has("posts", |q| q.db_where("published", "=", true)) |
shipped | Correlated EXISTS engine |
loadMissing |
user.load_missing(&["posts"]).await? |
shipped | Operates collection-wide |
| Cloning a record | user.replicate() / user.replicate_into::<OtherType>() |
shipped | Dispatches Replicating event |
| Touching parent timestamps | #[model(touches = ["post"])] |
shipped | One UPDATE per BelongsTo owner, one level deep and event-free (no grandparent recursion, no parent saved event). without_touching / without_touching_on::<M, _, _>() to skip. Parent touching |
| Observers | impl Observer<User> + #[suprnova::observer(User)] |
shipped | 16 lifecycle events |
| 16 lifecycle events | Created, Creating, Saving, Saved, Updating, Updated, Deleting, Deleted, Trashed, Restoring, Restored, Retrieved, Replicating, ForceDeleting, ForceDeleted, Pruning |
shipped | Per-model events::* submodule. EventResult::cancel(_) short-circuits with a 400 |
| Mutators / Accessors | #[accessor] fn full_name(&self) -> String { ... } + #[mutator] fn set_password(&mut self, v: String) |
shipped | Mutators |
| Casts (22 built-in) | casts! { AsString, AsInt, AsFloat, AsBool, AsJson, AsArray, AsArrayObject, AsObject, AsCollection, AsDate, AsDateTime, AsImmutableDate, AsImmutableDateTime, AsOptionalDateTime, AsTimestamp, AsDecimal, AsEnum<E>, AsEncrypted, AsEncryptedObject, AsEncryptedArray, AsEncryptedCollection, AsHashed } |
shipped | Implement Cast for custom |
| Collections | Collection<M> with pluck, filter, map, each, chunk, groupBy, keyBy, sort_by, where_, first, last, count, is_empty, to_array and Laravel friends; Deref<Target = Vec<M>> so all Vec idioms keep working |
shipped | Collections |
modelKeys() |
Builder::model_keys().await? (no hydration, qualified key) and Collection::model_keys() |
shipped | Both return Vec<M::Key>; the builder terminal projects users.id so it survives joins |
| API Resources | #[derive(Resource)] + IntoJsonResource + JsonApiResponse + fieldsets + includes |
shipped | JSON:API shape + Laravel-style resource shape both available. ?include= paths are capped at max_relationship_depth (default 5), matching JsonApiResource::$maxRelationshipDepth. API Resources |
| Serialization | #[model(hidden = [...], visible = [...], appends = [...])] |
shipped | Same control over which attributes serialise. Serialization |
| Factories | #[derive(Factory)] struct UserFactory + UserFactory::new().count(5).create().await? (or UserFactory::times(5).create_many().await?) |
shipped | Sequence for cycling values. Factories |
| Lifecycle: chunking / lazy / cursor | Builder::chunk(n, |page| async { ... }), lazy(), cursor() |
shipped | Memory-bounded iteration over large tables |
| Pessimistic locking | Builder::lock_for_update(), shared_lock() |
shipped | Inside a transaction |
refreshForUpdate() |
model.refresh_for_update().await? |
shipped | SELECT ... FOR UPDATE reload; no-op lock on SQLite. Row locking |
inOrderOf(col, values) |
Builder::in_order_of(col, values) |
shipped | Bound CASE WHEN ordering; unlisted values sort last. Typed builder only. Ordering |
orWhereKey / orWhereKeyNot |
Builder::or_where_key(id) / Builder::or_where_key_not(id) |
shipped | Fold into the preceding clause as a disjunction; or_filter_key / or_filter_key_not aliases |
whereJsonContains family |
Available via SeaORM's column expressions (driver-aware) | shipped | The exact spelling differs per backend; helpers ship for the common cases |
Pagination
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
LengthAwarePaginator |
LengthAwarePaginator (page + total + per_page + last_page) |
shipped | Builder::paginate(n).await? |
Paginator (simple) |
Paginator (page + per_page + has_more, no count) |
shipped | Builder::simple_paginate(n).await? |
CursorPaginator |
CursorPaginator (opaque cursor token + direction) |
shipped | Builder::cursor_paginate(n).await?; deterministic for infinite scroll |
| Inertia integration | IntoInertiaScroll trait + ScrollMetadata |
shipped | Wires straight into Inertia's WhenVisible / merge |
AI (Laravel ships native today; we don't gatekeep)
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| AI SDK | No first-party AI SDK | by design no | Bring the crate you already use (async-openai, anthropic-sdk, ollama-rs, tokenizers, etc.) and bind under App |
| MCP (Model Context Protocol) | No first-party MCP server adapter | by design no | The Rust MCP crates (mcp-rs, mcp-sdk-rust) sit cleanly under the existing routing / supervisor surface |
| Boost (Laravel coding agent) | n/a | by design no | Out of framework scope |
Testing
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
php artisan test |
cargo test |
shipped | Testing |
| Pest / PHPUnit style | #[suprnova_test] (async-aware) + expect!() Jest-like assertions + describe!() / test!() BDD macros |
shipped | All three work interchangeably |
| Feature tests (HTTP) | Drive handle_request(router, registry, req) in the same process, normally through a loopback hyper connection so the server receives a real Incoming body |
shipped | HTTP Tests |
TestResponse wrapper |
suprnova::testing::TestResponse - fluent assert_status / assert_json_path / assert_cookie / assert_session_has and friends, all chaining &Self |
shipped | HTTP Tests |
| Inertia testing helpers | suprnova::testing::AssertableInertia - component/url/version/prop/has/missing/where_/count/has_flash, plus reload_only/reload_except/load_deferred_props via a caller-supplied with_reload closure |
shipped | HTTP Tests |
| Console tests | Run dispatch_argv(["console", "..."]) and assert |
shipped | Same shape as HTTP tests for the console binary |
| Browser tests (Dusk) | n/a in framework - use Playwright / WebdriverIO / gstack agent browser |
by design no | Cross-language tooling already exists; we don't reinvent it |
| Database tests | TestDatabase::fresh::<Migrator>() |
shipped | Creates a fresh per-test in-memory SQLite database, applies migrations, registers it in the test container, and discards that isolated database/container state on drop; it does not wrap each test in a rollback transaction. Database Tests |
| Mocking & fakes | Per-facade fakes: MailFake, NotifyFakeGuard, EventFakeGuard, Queue::fake, Bus::fake, Http::fake, Storage::fake |
shipped | Recorded calls + assertion helpers. Mocking |
QueueFake job uuids |
queue::testing::pushed_with_id::<J>() |
shipped | The fake stamps an envelope id per push and emits the same JobQueued a real push does |
| Time travel | tokio::time::{pause, advance, resume} from the stdlib runtime |
shipped | Don't ship our own - Tokio's API already does it |
| Container isolation | TestContainer::fake(|tc| tc.bind(...)) - thread-local |
diverged | Parallel-safe by construction. Container |
Payments (Laravel's Cashier; ours is provider-generic)
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Cashier (Stripe) | suprnova-payments-stripe adapter crate behind generic Payment / Subscription / CustomerStore / WebhookHandler traits |
diverged | Generic surface, concrete adapter. Payments, Stripe Adapter |
| Cashier (Paddle) | suprnova-payments-paddle adapter |
diverged | Merchant-of-Record flow + no direct Payment impl (Paddle owns the gateway). Paddle Adapter |
| Custom provider | Implement PaymentProvider + SessionPayload + WebhookHandler |
shipped | Provider Guide |
| Inertia checkout components | Documented dispatch loops for Svelte / React / Vue against SessionPayload.flow |
shipped | Payments Frontend. Ready-made billing pages are a planned starter-kit addition (Starter Kits) |
| Subscription lifecycles | Subscription::subscribe / update / cancel / get (where the provider supports them) |
shipped | NotSupported returned where the provider doesn't (e.g. Paddle subscribe and price-set replacement) |
| Webhook idempotency | payments_webhook_events mirror table with UNIQUE(provider, provider_event_id) |
shipped | Stripe-style replay protection |
| Mirror tables | payments_customers, payments_payment_methods, payments_subscriptions, payments_subscription_items, payments_transactions, payments_webhook_events |
shipped | provider_metadata JSONB column on each for adapter-specific fields |
Frontend (Laravel has Blade + starter kits; we have Inertia)
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
| Blade | n/a - Inertia is the view layer | diverged | Frontend |
| Inertia.js | First-class: v3 over Svelte 5 / React 19 / Vue 3.5 | shipped | Inertia Responses, Pages |
Route::inertia($uri, $component, $props) |
Router::inertia(path, component, props) |
shipped | Returns a RouteBuilder, so .name(...) / .middleware(...) chain; Router::view is the older alias |
Page URL resolution (Inertia::resolveUrlUsing) |
page.url is path + query; override with InertiaConfig::url_resolver |
shipped | Default derivation matches the version middleware's X-Inertia-Location byte for byte; a url_resolver changes page.url only |
Inertia protocol middleware (Vary, empty response, version bounce) |
InertiaHeadersMiddleware + InertiaVersionMiddleware + Inertia303Middleware - three of the middlewares Inertia::install wires (validation-error redirect and error pages are the next two rows) |
shipped | Vary: X-Inertia on every response; empty 200 on an Inertia visit becomes 303 back; the 409 bounce re-flashes the session |
Validation-error redirect (Middleware::resolveValidationErrors, $withAllErrors) |
InertiaValidationRedirectMiddleware, wired by Inertia::install; InertiaConfig::with_all_errors(bool) |
shipped | A 422 on an Inertia visit becomes 303 back with the errors flashed; a field's value collapses to its first message unless with_all_errors(true). Inertia Responses |
Error pages (Inertia::render('Error', ['status' => …]) from the exception handler) |
InertiaConfig::error_page("Error"); InertiaErrorPageMiddleware, wired by Inertia::install when a component is named |
shipped | One config line instead of an exception-handler edit. A 403 / 404 / 429 / 500 on an Inertia visit or a browser navigation renders the named page at the original status, with status / message / request_id props; API clients, 422s, X-Inertia-Location bounces, and any body that is not an error envelope are untouched. The three starters ship the page and enable it. Inertia Responses |
| External redirect + history clearing | InertiaResponse::location_for(&req, url), App::clear_history() |
shipped | location_for is 409 for XHR and 302 for a hard navigation; App::clear_history() survives the logout redirect |
Inertia::share / getShared / flushShared |
App::inertia_share / _lazy / _once, App::inertia_shared(key), App::flush_inertia_shared() |
shipped | Dot-key nesting via Arr::set semantics; per-request InertiaSharedData::share(&req, component) can vary by page. A dotted share stays flat until the response's unpacking pass, so only/except match an ancestor entry (only: ['auth'] reaches auth.user) where Laravel gets the same result from Arr::set at share time |
| Partial reloads | #[derive(Data)] + req.includes("subset") + Inertia's partial-reload protocol |
shipped | Type-safe include sets. ?include= gates every lazy flavor including lazy(deferred), and runs ahead of X-Inertia-Partial-Data so a disallowed include still returns 400. errors is exempt from only/except, matching Laravel's Inertia::always share |
| Deferred props | .defer(…) / .defer_with(…, DeferOptions), or Prop::…defer() |
shipped | Inertia v3 deferred-props protocol; DeferOptions carries the group and the rescue flag. deferredProps ships on the initial visit only - resolveDeferredProps returns [] on any matched partial |
| Merge props | .merge / .merge_prepend / .deep_merge / .merge_with(MergeStrategy) / .merge_lazy / .merge_lazy_with, or Prop::…merge().merge_with_path(...) |
shipped | Inertia v3 merge protocol; match_on takes one field or several; merge_with_path merges a nested field instead of the prop's root |
Prop composition (defer()->merge(), merge()->once(), optional()->once()) |
Prop flag builder + InertiaResponse::prop(key, prop) |
shipped | Prop is a struct of orthogonal flags, mirroring the PHP adapter's Deferrable / Mergeable / Onceable interfaces |
| Encrypt history | EncryptHistoryMiddleware |
shipped | History encrypted at rest in the client |
| Scroll position | .scroll / .scroll_with / .scroll_wrapped / .paginate + ScrollMetadata / ProvidesScrollMetadata |
shipped | Auto-restore on navigation; reset reads X-Inertia-Reset, matching resolveScrollProps |
| TypeScript types | suprnova generate-types reads #[derive(InertiaProps)] and emits .d.ts |
shipped | TypeScript Types |
| Vite manifest reading | Auto-wired via InertiaConfig::manifest_path |
shipped | HMR in dev, hashed assets in prod. Inertia::install fails closed in production when the manifest is missing |
| Asset version from the build manifest | InertiaConfig default: VersionResolver::from_manifest(manifest_path) |
shipped | Hash of the manifest bytes; static "1.0" fallback when there is no build to hash |
Inertia SSR (inertia:start-ssr) |
InertiaConfig::ssr(...) on the config passed to Inertia::install, worker launched by suprnova ssr:start |
shipped | Out-of-process worker over HTTP loopback; falls back to CSR on error or timeout unless ssr_throw_on_error(true). InertiaConfig::ssr_bundle_path(...) gates dispatch on the built bundle existing on disk (mirrors ensure_bundle_exists), toggled with .ssr_ensure_bundle_exists(bool) (on by default once a bundle path is set); suprnova new scaffolds frontend/src/ssr.{ts,tsx} and a build:ssr script for every starter; suprnova ssr:check verifies the worker's GET /health route. Inertia Responses |
CLI
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
php artisan |
Per-app console binary built from #[command] macros |
shipped | Console, CLI overview |
make:controller / make:model / etc. |
suprnova make:controller / make:middleware / make:action / make:error / make:inertia / make:migration / make:task |
shipped | Generators |
serve |
suprnova serve (backend + Vite dev server together) |
shipped | Serve. Skips the Vite pane on a --api project instead of refusing to start. |
migrate family |
suprnova migrate / migrate:rollback / migrate:status / migrate:fresh |
shipped | Migrations CLI |
db:seed |
cargo run --bin console db:seed (via per-app console) |
shipped | Seeders registered via Seeder trait; a targeted run prints RUNNING / DONE with elapsed milliseconds |
schedule:run / schedule:work / schedule:list |
Same names via per-app console binary | shipped | Scheduling CLI |
queue:work |
Same name via per-app console binary | shipped | Graceful shutdown on SIGTERM/SIGINT |
tinker |
No REPL | by design no | See the row in "Digging deeper" |
Deployment
| Laravel | Suprnova | Status | Notes / link |
|---|---|---|---|
php artisan optimize |
cargo build --release |
diverged | One binary, no opcache step |
php artisan config:cache |
Typed config is compile-time-checked already | diverged | No runtime cache to invalidate |
php artisan route:cache |
Routes are macro-expanded at compile time | diverged | The router is built at boot from already-typed routes |
| Envoy (SSH deploys) | Use any orchestrator - Docker, systemd, Kubernetes, fly.io, Railway | by design no | The binary is the deploy artifact |
| Forge / Vapor | Not ours to ship - but the recipes for Railway, DO, and Hetzner cover the same job | diverged | Deployment, Railway, Digital Ocean, Hetzner |
Maintenance mode (php artisan down / up) |
./app down / ./app up - bypass secret with a server-checked 12-hour expiry, custom retry/message/except paths, file or cache driver |
shipped | Deployment |
| Horizon (queue dashboard) | No dashboard yet | not yet | Failed-job inspection via cargo run --bin console queue:failed until then |
Packages (Laravel's official packages - ours either ship in core, ship as adapters, or are deliberate gaps)
| Laravel package | Suprnova | Status | Notes / link |
|---|---|---|---|
| Cashier (Stripe) | suprnova-payments-stripe |
shipped | Generic + adapter. Payments |
| Cashier (Paddle) | suprnova-payments-paddle |
shipped | MoR flow. Payments |
| Dusk | n/a | by design no | Cross-language browser tooling already exists (Playwright, etc.) |
| Envoy | n/a | by design no | Containers / systemd / orchestrators do the job |
| Fortify | Replaced by auth_flows |
shipped | Same job, integrated. Auth Flows |
| Folio | n/a - page-based routing isn't idiomatic Rust | by design no | Use routes! for explicit routing |
| Homestead | n/a - use Docker / DevContainers | by design no | Docker recipe |
| Horizon | n/a yet | not yet | Failed jobs surface via the per-app console |
| Mix | Replaced by Vite | diverged | Vite ships in every scaffold |
| Octane | n/a - we are already long-lived Tokio | by design no | Single binary, always warm, no FPM to swap out |
| Passport | n/a yet | not yet | Run a dedicated IdP behind Suprnova until shipped |
| Pennant (feature flags) | Re-implemented as features::* |
shipped | Feature Flags |
| Pint (PHP code style) | cargo fmt + cargo clippy |
diverged | Standard Rust toolchain |
| Precognition | Inertia precognitive requests via partial reloads + the same #[derive(Data, Validate, FormRequest)] types |
shipped | The two halves of Precog (early validation + lightweight reload) both fall out of Inertia v3 + form requests |
| Prompts (CLI UI) | Use the dialoguer / inquire crate when needed |
by design no | Rust ecosystem already covers this |
| Pulse | n/a yet | not yet | OTel today, dashboard later |
| Reverb (WebSocket server) | Built into Suprnova (ws!() + BroadcastHub) |
diverged | No separate server needed - it's the same process |
| Sail (Docker dev) | suprnova-cli ships Docker recipes inline |
shipped | CLI Docker |
| Sanctum | BearerTokenMiddleware over Magnetar bearer sessions |
diverged | No separate package or personal-access-token management surface |
| Scout (full-text search) | n/a yet | not yet | Vector search ships (Vector); keyword Scout-equivalent later |
| Socialite | Magnetar provider registry and Auth::oauth(provider) |
shipped | OAuth |
| Telescope | n/a yet | not yet | Tracing + OTel cover the diagnostic gap until a dashboard ships |
| Valet | n/a - Rust apps run directly | by design no | suprnova serve is the dev runner |
Macros (Rust-specific surface; closest Laravel analogues for context)
Suprnova ships a wide set of proc-macros that don't have a Laravel analogue because Laravel doesn't have macros - it has runtime reflection. Including them here so you don't miss them.
| Macro | Closest Laravel idea | What it does |
|---|---|---|
#[suprnova::model] |
extends Model |
Generates SeaORM entity + impls Model trait |
#[suprnova::observer(M)] |
User::observe(UserObserver::class) |
Registers an Observer<M> impl via inventory |
#[scopes(M)] |
Local scopes on a model | Adds methods to Builder<M> |
#[accessor] / #[mutator] |
Eloquent accessors / mutators | Field-level get/set hooks |
#[handler] |
Controller __invoke |
Auto-extracts typed params from Request |
#[command] / #[derive(Command)] |
Artisan command class | Registers a console subcommand |
#[policy] |
Policy class | Registers a Policy impl via inventory |
#[service(T)] |
Service provider register |
Binds T into the container |
#[injectable] |
Constructor injection | Generates an App::make-backed constructor |
#[derive(InertiaProps)] |
Inertia props | TypeScript codegen + Inertia serialization |
#[derive(Data)] |
Request DTO | Extractable from Request with include-set support |
#[derive(FormRequest)] |
FormRequest class |
Validation + auth gate + transformation |
#[derive(Factory)] |
Model factory | Faker-backed test data generation |
#[derive(Resource)] |
API Resource | JSON:API + Laravel-shape serialization |
#[workflow] / #[workflow_step] |
n/a in Laravel | Long-running stateful work |
routes! + get! / post! / ws! etc. |
Route::get / Route::post |
Compile-time route registration |
casts! |
protected $casts = [...] |
Per-model cast declaration |
attrs! |
Mass-assignment array | Partial-attribute builder |
json_response! / text_response! |
response()->json(...) |
Quick Ok(HttpResponse::...) |
See Macros for the full reference.
Helper functions (Laravel's global helpers; ours are typed)
Laravel ships hundreds of small globals (str_replace_first, array_flatten,
now(), tap(), optional() …). Most of them have a direct Rust equivalent
in std or a small standard crate, so Suprnova doesn't reintroduce them as a
single namespace. The ones that are useful to have aliased ship under their
home module.
| Laravel helper | Suprnova / Rust equivalent | Where |
|---|---|---|
auth() |
Auth::user().await? |
Authentication |
cache() |
Cache::get/put/... |
Cache |
config('app.name') |
Config::get::<AppConfig>()?.name |
Configuration |
csrf_token() |
csrf_token() (same name) |
CSRF |
dd() |
Builder::dd() (Eloquent query dump-and-die) / dbg!() from the stdlib |
Builder::dump() / Builder::dd() exist for query inspection; use dbg!() for general values |
env('APP_KEY') |
env("APP_KEY") / env_required("APP_KEY") / env_optional("APP_KEY") |
Configuration, Env Vars |
now() |
chrono::Utc::now() (re-exported as suprnova::chrono) |
- |
optional($x)->y |
x.as_ref().map(|x| x.y) |
Rust handles this with Option<T> directly |
redirect('/') |
redirect("/") (same name) |
Routing |
request() |
Request is passed into your handler |
Requests |
response() |
HttpResponse::json/text/redirect/... |
Responses |
route('posts.show', ['post' => 1]) |
url("posts.show", &[("post", "1")]) |
URL Generation |
session('key') |
session().get("key") |
Session |
str() / Str::camel($x) |
heck crate methods (ToUpperCamelCase, etc.) |
- |
tap($x, fn) → $x |
tap from tap crate, or dbg! for quick inspection |
Use the tap crate idiomatically |
today() |
chrono::Utc::now().date_naive() |
- |
value($x) |
Just call the closure: x() |
n/a - Rust closures need no helper |
view('home', $data) |
Inertia response: Inertia::render("Home", data) |
Inertia Responses |
What we genuinely don't have yet
A consolidated list of every not yet above, so you can see the shape of the gap in one place:
| Area | What's missing | Workaround until shipped |
|---|---|---|
| Search (Scout - keyword) | Algolia / Meilisearch / Elastic adapter | Roll your own with meilisearch-sdk / elasticsearch until shipped; Vector handles semantic search today |
| Passport (OAuth server) | First-party OAuth identity provider | Run Hydra / Keycloak behind Suprnova |
| Telescope (debug dashboard) | Web UI for requests / queries / events / cache hits | Use OTel + tracing output (Observability) |
| Pulse (perf dashboard) | Web UI for slow queries / errors / hot routes | Same: OTel surface today, dashboard later |
| Horizon (queue dashboard) | Web UI for queue depth / failed jobs / throughput | cargo run --bin console queue:failed and OTel metrics |
What we won't ship (and why)
| Laravel feature | Why Suprnova doesn't have it |
|---|---|
| Tinker (REPL) | Rust doesn't have a productive REPL story for compiled binaries. A short #[suprnova_test] or a one-off cargo run --bin <thing> script does the job |
| Blade templates | Inertia is the view layer; we don't ship a parallel server-rendered template engine |
helpers.md kitchen-sink |
Rust ships std + small focused crates (heck, chrono, regex); we don't reintroduce a single global namespace |
| Mix | Vite covers it and ships in every scaffold |
| Octane | Suprnova is already long-lived Tokio; there's no FPM mode to optimise out of |
| Dusk (browser tests) | Cross-language tooling (Playwright, WebdriverIO, gstack agent browser) already solves this |
| Sail (Docker dev) | Docker recipes ship inline (CLI Docker); no separate package needed |
| Valet | suprnova serve is the dev server |
| Envoy (SSH deploys) | Containers / systemd / orchestrators do the job; we don't need a bespoke SSH DSL |
Concurrency facade (Concurrency::run) |
Tokio (tokio::join! / tokio::spawn / tokio::select!) is the answer; no facade needed |
| Processes facade | tokio::process::Command is already the right shape |
| First-party AI SDK / MCP / Boost | Pick the Rust crates you already use; we don't gatekeep |
| Dedicated Redis facade | Cache/queue/rate-limit cover 95% of typical use; reach for the redis crate when you need ad-hoc commands |
| Strings facade | heck, regex, std::str cover it; no Str::camel($x) global |
| Prompts (CLI UI library) | dialoguer / inquire already exist; we don't reinvent |
| Laravel-style PHP/JSON translation files | Localization ships, but the catalog format is Fluent .ftl - one format the server and the browser both parse. trans_choice has no equivalent either: Fluent selects CLDR plural categories inside the message. Localization |
php artisan dev --tabs (TUI multi-pane dev-process mode) |
Single-terminal, [name]-prefixed output is the Rust dev-tooling norm (cargo watch, bacon, just) - suprnova serve already gives every process (backend, frontend, and any Suprnova.toml entry) its own colored prefix and auto-restart. A tabbed TUI is a second interaction model for a signal this already provides; --stream's job - one scriptable, real-time output stream - ships as suprnova serve --json (NDJSON, one event per line). Serve |
How this list stays honest
Every row in the shipped column is verifiable by:
- Grepping
framework/src/lib.rsfor the named export - Running the framework test suite (
cargo test --workspace) - Reading the linked chapter
Every row in the not yet column is intended work, not a refusal. Every row in the by design no column has a one-sentence reason in the Notes column; those reasons are the design principles in Introduction applied to a specific feature.
Last reviewed against Laravel 13.25.0.
If you find a Laravel feature you reach for that isn't on this map, open an issue - it either has a Suprnova answer that's missing a row, or it's a real gap and we want to know.
Next
- From Laravel - the same map, narrated as a side-by-side
- Introduction - the design principles this parity work follows
documentation.md- the master TOC across every chapter
