# slave
A Laravel-inspired Rust framework for **MCP-first applications**.
Every business operation in a slave app is an
[MCP](https://modelcontextprotocol.io) tool served over Streamable HTTP, so AI
agents and your own web apps use the same, single API. The `slave` CLI
scaffolds the backend (users, roles, teams, passwordless sign-in,
[SurrealDB](https://surrealdb.com) with SurrealQL migrations), adds server-rendered web apps (MiniJinja, Tera, Handlebars or
Liquid templates), and serves them all from one process routed by domain or
path (any app can also run as a process of its own, proxied), in the spirit of `laravel new`, `php artisan make:*`
and `Route::domain()`.
```
browser / AI agent
│
▼
runtime (one process) ── config/apps.toml: route by domain and/or path
├──► MCP backend /mcp: auth_*, user_*, role_*, your tools … ← owns the database
└──► apps/storefront theme, server-rendered templates (calls MCP tools in-process)
```
## Why
- **One interface.** No REST layer next to MCP: the tools an AI agent calls are
the same ones your dashboards call.
- **Batteries included, no passwords.** Users and roles, sign-in with a
code sent by email or a passkey, server-side sessions that end at once on
sign-out, SurrealDB with SurrealQL migrations, and tests that drive `/mcp`
end to end with a throwaway database per test.
- **Pick a template language per app.** Server-rendered pages (MiniJinja,
Tera, Handlebars or Liquid) for a storefront theme or an admin area; apps
reach the backend only through MCP.
- **Laravel ergonomics.** Generators, optional domain and path routing,
`route:list`, `slave build` then one `slave serve` for everything.
- **Agent-ready projects.** Every new project ships an `AGENTS.md` (and a
`CLAUDE.md` that imports it) teaching coding agents the architecture, the
rules and recipes for adding features.
## Install
```sh
cargo install slave
```
Requirements: Rust 1.85+. Deploying to Fly.io needs the
[Fly CLI](https://fly.io/docs/flyctl/install/).
## Quick start
```sh
slave new shop && cd shop
surreal start --user root --pass root # in another terminal, or set SURREAL_* in .env
slave migrate # create the tables
slave build # the first build takes a few minutes, later ones seconds
slave serve # sign-in codes are printed in the log until SMTP_URL is set
```
Open <http://admin.localhost:8000> and sign in with any email; the code is
printed in the log until `SMTP_URL` and `MAIL_FROM` are set in `.env`. The
first user becomes super admin, and anyone can then add a passkey for quick
sign-in. `slave doctor` checks the setup and says what to fix. Add apps with
`slave make:app storefront --domain "{toko}.localhost"`.
Everything runs in one process, the runtime (`runtime/`). Without
`--domain`/`--path`, an app is served directly on its own port (still in that
process). `make:app --separate` or `app:separate <app>` runs an app as a
process of its own, proxied by the runtime, to deploy or scale it separately.
```
MCP backend http://127.0.0.1:3000/mcp
Runtime http://127.0.0.1:8000 (one process)
http://mcp.localhost:8000 → MCP backend
http://<toko>.localhost:8000 → storefront
```
MCP clients sign in with the `auth_email_start` and `auth_email_verify` tools
and send the session token they get, for example Claude Code:
```sh
claude mcp add --transport http shop http://mcp.localhost:8000/mcp
```
(with `--header "Authorization: Bearer $TOKEN"`).
## Commands
| `slave new <name>` | Create a project: MCP backend, auth, users, roles, migrations, the runtime, `AGENTS.md` |
| `slave build [<component>...] [--release]` | Compile everything, or only `backend`, `runtime` or a separate app, e.g. only what changed in CI (`--list` shows them) |
| `slave serve [--release]` | Run what was built: the runtime plus each separate app; never compiles |
| `slave doctor` | Check database, sign-in email, passkeys and build |
| `slave app:separate <app>`, `app:embed <app>` | Run an app as a process of its own (proxied by the runtime), or back inside it |
| `slave install:fly [--app <name>] [--region <code>] [--domain <host>]` | Fly.io files for the single runtime: `Dockerfile`, `fly.toml`, `config/apps.fly.toml` |
| `slave deploy:fly [--secrets <env file>]` | Create the Fly app if needed, stage secrets, check the required ones, `fly deploy` (migrations as the release command) |
| `slave make:tools <Name>Tools [--module m]` | MCP tools for a module (`src/modules/<m>/tools.rs`) |
| `slave make:model <Name> [--module m]` | A model in `src/modules/<m>/models/` plus a migration defining its table |
| `slave make:migration <name>` | An empty SurrealQL migration in `database/migrations/` |
| `slave login [--url]`, `slave logout` | Sign in to the project's backend (a code by email; needs `slave serve` running) |
| `slave make:app <name> [--team <slug>] [--project <path>] [--domain <host>] [--path </prefix>] [--customer] [--separate] [--stack minijinja\|tera\|handlebars\|liquid]` | A web app in `apps/<name>`, owned by one of your teams (requires `slave login`; outside a project, it uses the one you last logged in from); direct on its port, or routed by domain/path; in the runtime or separate |
| `slave migrate`, `migrate:status`, `migrate:fresh [--force]` | Apply pending migrations; list them; drop everything and re-apply (asks to confirm) |
| `slave route:list`, `route:match <host>[/path]` | Inspect domain and path routing |
## Database
The backend keeps the platform's own data (users, roles, teams, and what you
add) in [SurrealDB](https://surrealdb.com), on a server it reaches over
WebSocket: locally `surreal start --user root --pass root`, in production
SurrealDB Cloud or your own cluster. Set `SURREAL_URL`, `SURREAL_NS`,
`SURREAL_DB`, `SURREAL_USER`, `SURREAL_PASS` and `SURREAL_AUTH_LEVEL` in
`.env`, then `slave migrate`. Migrations are SurrealQL files in
`database/migrations/`, each applied once, in one transaction. Web apps never
open the database; they call MCP tools. Tests create a fresh database per
test on `TEST_SURREAL_URL` (default `ws://127.0.0.1:8000`, `root`/`root`)
and remove it afterwards.
## Deploying to Fly.io
```sh
fly auth login
cp .env .env.production # then edit it for production
slave install:fly --app tokoku --domain example.com # optional: deploy:fly runs it if needed
slave deploy:fly --secrets .env.production
fly certs add "*.example.com" --app tokoku # one per host install:fly prints
```
`install:fly` writes a `Dockerfile`, `fly.toml` and `config/apps.fly.toml`
(your routes with `localhost` moved under `--domain`). `deploy:fly` creates
the app, stages the env file as Fly secrets, refuses to deploy while a
required secret is missing, and runs `fly deploy`; pending migrations run
first (`release_command`). Machines are stateless, so `fly scale count 2`
works. Environment on Fly:
| `SURREAL_URL` | yes | The platform's SurrealDB, e.g. `wss://<instance>.surreal.cloud`. Migrations run against it on every deploy (`release_command`) |
| `SURREAL_USER`, `SURREAL_PASS` | yes | A SurrealDB user; prefer a database user (`DEFINE USER ... ON DATABASE`, `SURREAL_AUTH_LEVEL=database`) |
| `SURREAL_NS`, `SURREAL_DB`, `SURREAL_AUTH_LEVEL` | no | Namespace and database (default `main`); where the user is defined (`root` by default) |
| `SMTP_URL`, `MAIL_FROM` | yes | Where sign-in codes are sent from, e.g. `smtps://user:pass@smtp.resend.com` and `Shop <no-reply@shop.com>` |
| `PASSKEY_RP_ID`, `PASSKEY_ORIGIN` | yes | The domain passkeys belong to (shared by its subdomains), e.g. `shop.com` and `https://shop.com` |
| `<APP>_PUBLIC_URL` | no | Fixed public origin of an app (otherwise taken from the request) |
| `MCP_ALLOWED_HOSTS` | no | Only if MCP clients reach the backend directly instead of through a routed host |
| `APP_NAME`, `RUST_LOG` | no | Set in `fly.toml` / the image (`info`) |
Set by the image, do not override: `APP_HOST`, `APP_PORT`, `GATEWAY_LISTEN`
(`0.0.0.0:8080`), `SLAVE_ROUTES` (`config/apps.fly.toml`) and
`SLAVE_TRUSTED_PROXY` (`1`, so apps see `https` behind Fly's proxy).
## Routing
`config/apps.toml` maps requests to apps, like Laravel's `Route::domain()` and
`Route::prefix()`:
```toml
[[domain]]
host = "admin.localhost" # by domain
app = "dashboard"
port = 4000
[[domain]]
host = "{toko}.localhost" # {toko} reaches the app as X-Slave-Param-Toko
app = "storefront"
port = 4001
[[domain]]
path = "/console" # by path, on any host (add `host` to restrict)
app = "console"
port = 4002
```
The most specific entry wins regardless of order: the longest path prefix,
then the most specific host. Unknown routes get 404, a down app gets 502, and
spoofed `X-Slave-*` / `X-Forwarded-*` headers are dropped.
## Crates
| [`slave`](https://crates.io/crates/slave) | This CLI and the project templates |
| [`slave-framework`](https://crates.io/crates/slave-framework) | Runtime used by generated apps: server lifecycle, config, logging, the SurrealDB connection and migrations, passwordless sign-in (email codes, passkeys, sessions), and a test database |
| [`slave-gateway`](https://crates.io/crates/slave-gateway) | Domain/path routing for the runtime: apps in-process, a hyper proxy for separate apps |
## Repository layout
```
src/ the slave CLI (commands, generators)
templates/ files written by `slave new` and `slave make:*`
framework/ slave-framework
gateway/ slave-gateway
tests/ end-to-end tests of the CLI and generated projects
```
Run the tests with `cargo test --workspace`; `cargo test -p slave --test cli
-- --ignored` also builds a generated project and runs its own test suite.
## License
MIT