Manual contentsGetting StartedBrowse 103 chapters
Manual 9 min read

Directory Structure

When you run suprnova new my-app --frontend svelte, the scaffolder gives you this:

my-app/
├── Cargo.toml                      # crate manifest + dependencies, two [[bin]] targets
├── .env                            # local config - DB URL, app key, ports
├── .env.example                    # template for ops/CI
├── .gitignore                      # excludes target/, .env, node_modules/, public/assets/
├── cmd/
│   └── main.rs                     # the binary entry; calls Application::new().run()
├── src/
│   ├── lib.rs                      # module wiring (`pub mod controllers;` etc.)
│   ├── bootstrap.rs                # registers services, observers, listeners - the
│   │                               # Suprnova analogue of Laravel's service providers
│   ├── routes.rs                   # the `routes!` macro tree - every URL the app serves
│   ├── bin/
│   │   └── console.rs              # `cargo run --bin console <subcommand>` entry -
│   │                               # the Suprnova analogue of `php artisan`
│   ├── actions/
│   │   ├── mod.rs
│   │   └── example_action.rs       # one-method invokable controllers
│   ├── commands/
│   │   └── mod.rs                  # `#[command]`-annotated handlers register here
│   ├── config/
│   │   ├── mod.rs
│   │   ├── database.rs             # typed DB config (driver, URL, pool)
│   │   └── mail.rs                 # typed mail config
│   ├── controllers/
│   │   ├── mod.rs
│   │   ├── home.rs                 # GET / handler
│   │   ├── auth.rs                 # login / register / logout
│   │   └── dashboard.rs            # requires auth; example protected route
│   ├── middleware/
│   │   ├── mod.rs
│   │   ├── logging.rs              # request/response logging
│   │   └── authenticate.rs         # session-based auth guard
│   ├── migrations/
│   │   ├── mod.rs
│   │   ├── m_*_create_users_table.rs
│   │   ├── m_*_create_sessions_table.rs
│   │   ├── m_*_create_remember_tokens_table.rs
│   │   ├── m_*_create_workflows_table.rs
│   │   └── m_*_create_workflow_steps_table.rs
│   └── models/
│       ├── mod.rs
│       └── user.rs                 # `#[suprnova::model]` User model
├── frontend/
│   ├── package.json
│   ├── vite.config.ts
│   ├── tsconfig.json
│   ├── index.html                  # Vite entry; mounts the SPA
│   └── src/
│       ├── main.{tsx,ts}           # Inertia client setup (per-framework)
│       ├── app.css                 # global styles + Tailwind
│       ├── pages/
│       │   ├── Home.{tsx,svelte,vue}
│       │   ├── Dashboard.{tsx,svelte,vue}
│       │   └── auth/
│       │       ├── Login.{tsx,svelte,vue}
│       │       └── Register.{tsx,svelte,vue}
│       └── types/
│           └── inertia-props.ts    # auto-generated from #[derive(InertiaProps)]
└── public/
    └── assets/                     # Vite production build output lands here

Svelte adds frontend/svelte.config.js and frontend/src/app.d.ts. Vue adds frontend/src/shims-vue.d.ts.

The API starter (suprnova new my-api --api) is slimmer: no frontend/, no auth controllers, and cmd/main.rs is replaced by src/main.rs.

What each directory is for

cmd/main.rs

The binary entry point. A short file - typically 10–20 lines - that calls the standard boot pipeline:

use suprnova::Application;
use my_app::{bootstrap, config, migrations, routes};

#[suprnova::main]
async fn main() {
    Application::new()
        .config(config::register_all)
        .bootstrap(bootstrap::register)
        .routes(routes::register)
        .migrations::<migrations::Migrator>()
        .run()
        .await;
}

Application::run() parses the binary's CLI (serve / web:run / migrate* / schedule:* / workflow:work / queue:work), loads .env, runs your config function, then dispatches the subcommand. The serve path also runs your bootstrap function and starts the HTTP server.

You almost never edit cmd/main.rs after the initial scaffold.

src/lib.rs

A flat module declaration file:

pub mod actions;
pub mod bootstrap;
pub mod commands;
pub mod config;
pub mod controllers;
pub mod middleware;
pub mod migrations;
pub mod models;
pub mod routes;

This is what makes crate::controllers::home::index reachable from routes.rs.

src/bootstrap.rs

The single function that wires your app. You register service container bindings, observers, event listeners, custom middleware, and any other boot-time setup here. It's the analogue of Laravel's AppServiceProvider, EventServiceProvider, BroadcastServiceProvider, etc., all in one file:

use std::sync::Arc;
use suprnova::App;

pub async fn register() {
    // Bind a service into the container
    App::bind::<dyn MyService>(Arc::new(MyServiceImpl::new()));

    // Register an Eloquent observer
    crate::models::user::register_observer();

    // Listen for events
    suprnova::Event::listen::<OrderShipped, _>(Arc::new(SendShipmentNotification)).await;
}

register() runs once per process, after the config loader but before serve accepts the first request. Workers (queue:work, schedule:run, workflow:work) reuse the same bootstrap so they see the same services. See Application Bootstrap.

src/routes.rs

Your URL surface. The routes! macro at module top-level expands into a pub fn register() -> Router that cmd/main.rs hands to Application::routes(...):

use suprnova::{get, post, put, delete, routes};
use crate::{controllers, middleware};

routes! {
    get!("/", controllers::home::index).name("home"),

    // Auth (registered + protected)
    get!("/login", controllers::auth::show_login).name("login.show"),
    post!("/login", controllers::auth::login).name("login.attempt"),
    post!("/logout", controllers::auth::logout).name("logout"),
    get!("/register", controllers::auth::show_register).name("register.show"),
    post!("/register", controllers::auth::register).name("register"),

    // Dashboard requires authenticate middleware
    get!("/dashboard", controllers::dashboard::index)
        .middleware(middleware::authenticate::auth())
        .name("dashboard"),
}

See Routing.

src/bin/console.rs

Your per-project console binary. Runs as cargo run --bin console <subcommand> and dispatches the framework's db:seed built-in plus every #[command]-annotated handler (or #[derive(Command)] typed struct) in src/commands/ - both forms register through inventory at compile time:

cargo run --bin console db:seed           # framework built-in
cargo run --bin console report:daily      # your custom command

The long-running workers (queue:work, schedule:run, schedule:work, workflow:work) live on the main app binary because Application::run() dispatches them - call them as cargo run -- queue:work (or via suprnova schedule:run / suprnova workflow:work if you prefer the umbrella CLI).

See Console.

src/commands/

Where your console handlers live. Two flavours: a typed struct with clap-derived args and impl TypedCommand, or a raw #[command] on an async fn(Vec<String>) -> Result<(), FrameworkError>. The scaffolder generates the typed form:

use async_trait::async_trait;
use clap::Parser;
use suprnova::{Command, FrameworkError, TypedCommand};

#[derive(Parser, Command, Debug)]
#[console(name = "report:daily", description = "Generate the daily report")]
pub struct DailyReport {
    #[arg(long, default_value_t = false)]
    pub dry_run: bool,
}

#[async_trait]
impl TypedCommand for DailyReport {
    async fn run(self) -> Result<(), FrameworkError> {
        //        Ok(())
    }
}

suprnova make:command report-daily scaffolds the file and adds it to src/commands/mod.rs. See Console.

src/config/

Typed configuration structs. The scaffold ships database.rs and mail.rs; add your own for any subsystem your app cares about. Each config struct reads its values from the environment, and config::register_all() registers them with the framework:

use suprnova::{env, env_required};

#[derive(Clone, Debug)]
pub struct AnalyticsConfig {
    pub api_key: String,
    pub max_batch: u32,
}

impl AnalyticsConfig {
    pub fn from_env() -> Self {
        Self {
            api_key: env_required::<String>("ANALYTICS_API_KEY"),
            max_batch: env("ANALYTICS_MAX_BATCH", 100u32),
        }
    }
}

Wire it in config/mod.rs:

use suprnova::Config;

pub fn register_all() {
    Config::register(AnalyticsConfig::from_env());
}

See Configuration.

src/controllers/

HTTP handler functions. One module per resource. Each pub async fn that takes a Request and returns a Response is callable from a route.

src/middleware/

Middleware implementations. The scaffold ships logging and authenticate; you add your own here as pub struct Foo with impl Middleware for Foo. Register them globally in bootstrap.rs or apply per-route via .middleware(…) in the routes! tree. See Middleware.

src/migrations/

SeaORM migrators. The scaffold ships a handful for the auth + workflow tables. suprnova make:migration <name> adds a new one. suprnova migrate, migrate:rollback, migrate:status, migrate:fresh, db:sync all operate on this directory. See Migrations.

src/models/

Your Eloquent models. One file per model, each a #[suprnova::model] struct. The scaffold ships user.rs; add new models by writing a new file by hand or running suprnova db:sync --regenerate-models after a schema migration. See Eloquent.

src/actions/

Single-method invokable controllers. Optional pattern - use them when a controller would have exactly one method and you'd rather call it "Action" than wrap it. The scaffold ships an example you can delete or adapt. See Actions.

frontend/

The Vite + Inertia SPA. This is a normal frontend project - package.json, vite.config.ts, tsconfig.json, an index.html Vite entry, source under src/. The Inertia client setup lives in src/main.{tsx,ts} and the page components in src/pages/. TypeScript types for your Rust #[derive(InertiaProps)] props are regenerated into src/types/inertia-props.ts by suprnova generate-types.

See Frontend.

public/assets/

Where Vite drops the production build (npm run build). The Suprnova server serves this directory as static assets at /assets/* in production.

Directories you'll add as the app grows

The scaffold gives you the minimum - enough to ship the welcome flow and a protected dashboard. Real apps grow more subsystems. Common additions:

Directory When you add it
src/jobs/ First time you Queue::push(SomeJob). See Queues.
src/listeners/ First time you Event::listen. See Events.
src/observers/ First time you implement Observer<MyModel>. See Eloquent.
src/notifications/ First time you implement a Notification. See Notifications.
src/mail/ First time you implement a Mailable. See Mail.
src/policies/ First time you write a #[policy]. See Authorization.
src/factories/ First time you write a Factory<Model> for tests. See Eloquent Factories.
src/seeders/ First time you write a Seeder for db:seed. See Seeding.
src/events/ First time you impl Event for your own event type. See Events.
src/broadcasting/ First time you define a private/presence Channel. See Broadcasting.
src/ws/ First time you write a ws!() handler. See WebSockets.
src/supervisors/ First time you implement a long-running Supervisor. See Supervisors.
src/payments/ First time you wire up Stripe/Paddle for your app. See Payments.
src/props/ When you want to keep #[derive(InertiaProps)] structs separate from controllers.
resources/views/ First time you add a Tera template for mail bodies.
storage/ First time you write files to the local filesystem disk (see File Storage).
tests/ First time you write an integration test.

You don't have to ask permission - mkdir src/jobs and add pub mod jobs; to src/lib.rs, and you're done. The framework doesn't enforce the directory names; the conventions exist so other Suprnova developers can find things quickly.

The dogfood app/ in this repo

If you're reading this from inside the Suprnova repo itself, you'll see an app/ directory at the root that uses every framework feature together. That's our internal test bed - it exercises payments, broadcasting, web push, workflows, supervisors, etc. all at once. It's NOT a clean reference for a new app; the scaffold output above is deliberately smaller and easier to learn from. Read app/ once you want to see a maximal example of how the pieces compose.

Next