Skip to content

Sp0Q1/fracture-cms

Repository files navigation

Fracture CMS

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.

What You Get

  • 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 by org_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_token to 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-core binary. Place a same-named file in your assets/views/ directory to override any of them.
  • i18n — Fluent-based internationalization with locale files in assets/i18n/.

Quick Start

Prerequisites

  • Podman and podman-compose
  • Rust (only needed for local cargo development outside containers)

1. Clone and set up

git clone <repo-url> my-project
cd my-project
./dev/setup.sh            # Starts Zitadel, creates OIDC app, creates test user, writes .env

2. Start the app

podman compose up -d mailcrab app

3. Open the 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.

4. Sign in and explore

  1. Open http://localhost:5150 and click Get Started
  2. Sign in with the test user credentials
  3. You land on the dashboard — a personal org was created for you automatically
  4. Go to Organizations to create a team org
  5. Invite a colleague (or yourself with a different email) from the Members page
  6. Check http://localhost:1080 to see the invitation email
  7. Create a project, add some notes — all scoped to the active org
  8. Switch between orgs using the dropdown in the nav bar

Local development (without containers)

cp .env.example .env
# Fill in JWT_SECRET, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_PROJECT_ID
cargo loco start

This requires a running OIDC provider and SMTP server configured in .env.

How It Works

Authentication

The app never handles passwords. All authentication is delegated to an OIDC provider:

  1. User clicks "Sign in" and is redirected to the IdP with a PKCE challenge
  2. After authenticating, the IdP redirects back with an authorization code
  3. The app exchanges the code for an ID token, verifies the signature via JWKS, and checks audience claims
  4. A JWT session cookie is set (HTTP-only, SameSite=Lax)
  5. On first login, a user record and personal org are created. Pending invites matching the email are auto-accepted.

Organizations & Roles

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.

Invite Flow

  1. Admin enters an email and role on the members page
  2. An invite record is created (expires in 7 days) and an email is sent via SMTP
  3. The accept link is also shown on the page so it can be copied directly
  4. Existing users click the link to join. New users are auto-added when they first sign in with a matching email.

Project Structure

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)

Routes

Authentication

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

Organizations

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)

Projects (org-scoped example)

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

Notes (project-scoped example)

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

Building on This

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.

CI

GitHub Actions runs 4 checks: rustfmt, clippy (with pedantic lints), semgrep, and tests.

To run the same checks locally:

./dev/ci.sh

Tech Stack

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •