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. - Template overrides — Core org templates (list, new, settings, members, invite accept) are embedded in the
fracture-corebinary. Place a same-named file in yourassets/views/directory to override any of them. - 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
models/
_entities/ # Core SeaORM entities (users, orgs, members, invites)
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
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)
mailers/
invite.rs # Invitation email (SMTP via background worker)
lib.rs # Module exports + register_templates()
templates/org/ # Embedded HTML templates (overridable by app)
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)
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 |
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 |