A multi-tenant web application starter built with Rust on the Loco framework. Provides OIDC authentication, organization management with role-based access control, email invites, and org-scoped data isolation out of the box. Ships with projects and notes as example resources to show the patterns.
The core infrastructure lives in the fracture-core library crate. Downstream projects depend on it as a Cargo dependency and only write their own domain code — no forking, no merge conflicts on core updates.
- OIDC single sign-on — Delegates authentication to any OpenID Connect provider (Zitadel, Keycloak, Auth0, etc.). No passwords stored in your database. Uses PKCE authorization code flow.
- Organizations — Each user gets a personal org on first login. Users can create team orgs and invite members by email.
- Role-based access control — Four roles (Owner > Admin > Member > Viewer) enforced at the controller level via
require_role!macro. All database queries scoped byorg_id. - Email invites — Admins invite users by email. Invites expire after 7 days. If the invitee doesn't have an account yet, the invite is auto-accepted when they sign in with a matching email.
- Session management — JWT stored in HTTP-only cookies. The frontend refreshes the token every 12 minutes; on failure, the user sees a "session expired" message with a re-login link.
- Security headers — Content-Security-Policy (
default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'), plus X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and X-Permitted-Cross-Domain-Policies. - Back-channel logout — The IdP can POST a signed
logout_tokento invalidate a user's session server-side. The app verifies the token signature via JWKS before acting on it. - File uploads — Org-scoped file upload API with configurable size limits, MIME type validation, SHA-256 checksums, and visibility control (
orgorpublic). Files stored on disk under a configurable storage root. - Blog system — Markdown-based blog with GFM support (tables, strikethrough, task lists). Posts tied to a configurable blog org. Public routes for readers, admin routes for platform admins. Markdown rendered to HTML on save via comrak.
- Generic jobs system — Define job types, schedule runs, and track diffs. Apps implement the
JobExecutortrait; fracture-core handles the execution lifecycle, run history, and diff storage. Includes both org-scoped and platform admin views. - Template overrides — Core templates (org, blog, jobs) are embedded in the
fracture-corebinary. Place a same-named file in yourassets/views/directory to override any of them. - Markdown editor hook — Blog admin templates use the
data-md-editorattribute on textareas. Consuming apps provide their ownmd-editor.jsto initialize a Markdown editor (toolbar, preview, etc.) for elements with this attribute. fracture-core does not bundle an editor implementation. - i18n — Fluent-based internationalization with locale files in
assets/i18n/.
git clone <repo-url> my-project
cd my-project
./dev/setup.sh # Starts Zitadel, creates OIDC app, creates test user, writes .envpodman compose up -d mailcrab app| Service | URL | Purpose |
|---|---|---|
| App | http://localhost:5150 | Your application |
| Zitadel | http://localhost:8080 | Identity provider admin console |
| MailCrab | http://localhost:1080 | Catches all outbound email for testing |
A test user is created automatically by setup.sh. Credentials are printed at the end of the script.
- Open http://localhost:5150 and click Get Started
- Sign in with the test user credentials
- You land on the dashboard — a personal org was created for you automatically
- Go to Organizations to create a team org
- Invite a colleague (or yourself with a different email) from the Members page
- Check http://localhost:1080 to see the invitation email
- Create a project, add some notes — all scoped to the active org
- Switch between orgs using the dropdown in the nav bar
cp .env.example .env
# Fill in JWT_SECRET, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_PROJECT_ID
cargo loco startThis requires a running OIDC provider and SMTP server configured in .env.
The app never handles passwords. All authentication is delegated to an OIDC provider:
- User clicks "Sign in" and is redirected to the IdP with a PKCE challenge
- After authenticating, the IdP redirects back with an authorization code
- The app exchanges the code for an ID token, verifies the signature via JWKS, and checks audience claims
- A JWT session cookie is set (HTTP-only, SameSite=Lax)
- On first login, a user record and personal org are created. Pending invites matching the email are auto-accepted.
Every piece of data belongs to an organization. The active org is tracked via an org_pid cookie.
| Role | View | Create/Edit/Delete | Invite Members | Org Settings |
|---|---|---|---|---|
| Viewer | Yes | No | No | No |
| Member | Yes | Yes | No | No |
| Admin | Yes | Yes | Yes | Yes |
| Owner | Yes | Yes | Yes | Yes |
Roles are enforced in every controller via require_role!(org_ctx, OrgRole::Member). All database queries are scoped by org_id — there is no code path that returns data across orgs.
- Admin enters an email and role on the members page
- An invite record is created (expires in 7 days) and an email is sent via SMTP
- The accept link is also shown on the page so it can be copied directly
- Existing users click the link to join. New users are auto-added when they first sign in with a matching email.
fracture-core/ # Library crate (reusable across projects)
src/
controllers/
middleware.rs # JWT auth, OrgContext, require_user!/require_role! macros
oidc.rs # OIDC login, logout, back-channel logout
oidc_state.rs # OIDC state store (CSRF tokens, PKCE verifiers)
org.rs # Organization CRUD, members, invites, switching
uploads.rs # File upload API (create, serve, delete)
blog.rs # Blog public + admin routes
jobs.rs # Job definitions, runs, diffs, trigger
models/
_entities/ # Core SeaORM entities
users.rs # User lookup, OIDC account creation/linking
organizations.rs # Org creation, personal orgs, slug lookup
org_members.rs # Membership, OrgRole enum, role hierarchy
org_invites.rs # Email invitations, auto-accept on signup
uploads.rs # Upload queries, Visibility enum
blog_posts.rs # Blog queries, Markdown rendering
job_definitions.rs # Job definition queries
job_runs.rs # Job run lifecycle
job_run_diffs.rs # Diff queries
upload/
config.rs # UploadConfig (size limits, allowed types, storage root)
service.rs # UploadService (validation, storage, checksums)
jobs/
mod.rs # JobExecutor trait, JobRegistry, JobResult, JobDiff
initializers/
oidc.rs # OIDC discovery, client setup, JWKS URI
security_headers.rs # CSP, X-Frame-Options, etc.
views/
org.rs # Org view helpers (list, settings, members)
blog.rs # Blog view helpers (public + admin)
jobs.rs # Jobs view helpers (org + admin)
mailers/
invite.rs # Invitation email (SMTP via background worker)
lib.rs # Module exports + register_templates()
templates/org/ # Embedded org templates (overridable by app)
templates/blog/ # Embedded blog admin templates (overridable by app)
static/upload.js # Upload helper script
migration/src/ # Core database migrations
src/ # App (your domain-specific code)
controllers/
home.rs # Dashboard
project.rs # Project CRUD (org-scoped) — example resource
note.rs # Note CRUD (project-scoped) — example resource
fallback.rs # 404 handler
models/
_entities/ # App entities + re-exports of core entities
projects.rs # Org-scoped project queries
notes.rs # Project-scoped note queries
views/ # View helpers (Rust -> template context)
initializers/
view_engine.rs # Tera templates + Fluent i18n + core template registration
mailers/ # Re-exports core mailers + app-specific mailers
app.rs # Route registration, hooks
migration/src/ # App-specific migrations (projects, notes)
assets/
views/ # App Tera templates (can override core templates)
static/ # CSS, JS, images
i18n/ # Fluent locale files (en-US, de-DE)
fracture-ctl/ # CLI tool for deployment management
src/main.rs # init, up, down, backup, restore, admin, ci, dev, update
config/ # Loco YAML config per environment
docs/ # Architecture, template guide, resource recipes
dev/
setup.sh # Provisions Zitadel + writes .env
ci.sh # Runs all CI checks locally in containers
Dockerfile.ci # CI container image (Rust + SQLite + clippy + rustfmt)
| Method | Path | Description |
|---|---|---|
| GET | /api/auth/oidc/authorize |
Start OIDC login flow |
| GET | /api/auth/oidc/callback |
OIDC callback (exchanges code for token) |
| GET | /api/auth/oidc/logout |
Clear session and redirect to IdP logout |
| GET | /api/auth/oidc/refresh |
Refresh JWT cookie |
| POST | /api/auth/oidc/backchannel-logout |
IdP-initiated session invalidation |
| Method | Path | Min Role |
|---|---|---|
| GET | /orgs |
(any authed) |
| POST | /orgs |
(any authed) |
| GET | /orgs/new |
(any authed) |
| GET | /orgs/{pid}/settings |
Admin |
| POST | /orgs/{pid}/settings |
Admin |
| GET | /orgs/{pid}/members |
Viewer |
| POST | /orgs/{pid}/members/invite |
Admin |
| POST | /orgs/{pid}/members/{user_pid}/role |
Admin |
| POST | /orgs/{pid}/members/{user_pid}/remove |
Admin |
| GET | /orgs/switch/{pid} |
(member) |
| GET | /invites/{token}/accept |
(any authed) |
| Method | Path | Min Role |
|---|---|---|
| GET | /projects |
Viewer |
| POST | /projects |
Member |
| GET | /projects/new |
Member |
| GET | /projects/{pid} |
Viewer |
| GET/POST | /projects/{pid}/edit |
Member |
| DELETE | /projects/{pid} |
Member |
| Method | Path | Min Role |
|---|---|---|
| POST | /projects/{pid}/notes |
Member |
| GET | /projects/{pid}/notes/new |
Member |
| GET | /projects/{pid}/notes/{note_pid} |
Viewer |
| GET/POST | /projects/{pid}/notes/{note_pid}/edit |
Member |
| DELETE | /projects/{pid}/notes/{note_pid} |
Member |
| Method | Path | Auth |
|---|---|---|
| POST | /api/uploads |
Authenticated |
| GET | /api/uploads/{pid} |
Public or org member |
| DELETE | /api/uploads/{pid} |
Uploader / org admin |
| Method | Path | Auth |
|---|---|---|
| GET | /blog/ |
Public |
| GET | /blog/{slug} |
Public |
| GET/POST | /admin/blog/... |
Platform admin |
| Method | Path | Auth |
|---|---|---|
| GET | /jobs |
Authenticated |
| GET | /jobs/{pid} |
Authenticated |
| POST | /jobs/{pid}/run |
Authenticated |
| GET | /jobs/{pid}/runs/{run_pid} |
Authenticated |
| GET | /admin/jobs |
Platform admin |
CLI tool for managing deployments. Install from GitHub Releases. See docs/DEPLOYMENT.md for the full walkthrough.
| Command | Description |
|---|---|
init --image <img> [--repo <url>] |
Generate production config (.env.prod + compose.prod.yaml) |
up |
Pull latest image, auto-backup, start services |
down |
Stop all services |
backup [-o file] |
Back up the database |
restore <file> [--yes] |
Restore from backup |
admin set <email> |
Promote user to platform admin |
admin list |
List platform admins |
ci |
Run CI checks via dev/ci.sh |
dev [--setup] |
Start the dev stack |
update |
Self-update to latest release |
See docs/TEMPLATE_GUIDE.md for how to create a new project using fracture-core as a library dependency.
See docs/ADDING_RESOURCES.md for a step-by-step recipe for adding new org-scoped resources (replace projects/notes with your domain).
See docs/UPSTREAM_UPDATES.md for updating fracture-core in your project.
See docs/DEPLOYMENT.md for production deployment instructions.
GitHub Actions runs 4 checks: rustfmt, clippy (with pedantic lints), semgrep, and tests.
To run the same checks locally:
./dev/ci.sh| Language | Rust |
| Framework | Loco (built on Axum) |
| Database | SQLite via SeaORM (PostgreSQL also supported) |
| Templates | Tera + Fluent i18n |
| Auth | OpenID Connect via openidconnect-rs |
| CSS | oat.ink (semantic HTML styling, no build step) |
| IdP | Any OIDC provider (Zitadel ships in the dev stack; Keycloak, Auth0, etc. also work) |
| Containers | Podman |