Source Tree Analysis
Multi-part Cargo workspace. Two members (backend, frontend) plus a workspace-excluded license-tool crate at deploy/license/.
shiftit/
├── Cargo.toml # Workspace manifest (members: backend, frontend; excludes deploy/license)
├── Cargo.lock # Committed
├── CLAUDE.md # Project handover doc — read this first
├── .gitlab-ci.yml # CI: fmt, clippy, backend test (with PG service), trunk build, cargo build
├── .pre-commit-config.yaml # fmt + per-crate clippy (backend stable, frontend nightly+wasm32)
├── .sqlx/ # sqlx offline query cache (committed; CI sets SQLX_OFFLINE=true)
│
├── backend/ # Part: backend (project_type_id=backend)
│ ├── Cargo.toml # Stable Rust; features { default = [], saas = [] }
│ ├── migrations/ # 0001..0007 — applied at startup via sqlx::migrate!
│ └── src/
│ ├── main.rs # ENTRY — create_router(), connect_db(), license check_on_startup
│ ├── openapi.rs # utoipa::OpenApi derive — every #[utoipa::path] handler listed here
│ ├── auth/ # JWT, JWKS, license, middleware
│ │ ├── claims.rs # Decoded JWT claims shape
│ │ ├── jwks.rs # JWKS fetcher + cache (Zitadel)
│ │ ├── license.rs # ed25519 license check (saas-feature-gated)
│ │ └── middleware.rs # auth_middleware → injects TenantContext
│ ├── shared/ # Cross-domain primitives
│ │ ├── error.rs # ApiError → axum response
│ │ ├── pagination.rs # Page<T> envelope (unused so far)
│ │ └── tenant_context.rs # TenantContext, UserRole, rls_transaction()
│ ├── tenants/ # Tenant settings
│ │ ├── db.rs # Includes rls_blocks_cross_tenant_reads test
│ │ ├── model.rs # TenantSettings DTO
│ │ └── routes.rs # GET/PATCH /tenant/settings
│ ├── shifts/ # Largest domain — roles, locations, users, events, shifts, slots, assignments
│ │ ├── hierarchy.rs # Role-hierarchy walking
│ │ ├── nominatim.rs # Server-side geocoding for Location.lat/lng
│ │ ├── model.rs # All shift-domain DTOs (RoleDefinition, Location, Event, Shift, ShiftSlot, …)
│ │ ├── db/ # One file per aggregate
│ │ │ ├── assignments.rs # claim_slot, assign_slot (P1: claim_slot race)
│ │ │ ├── events.rs
│ │ │ ├── locations.rs
│ │ │ ├── roles.rs
│ │ │ ├── shifts.rs
│ │ │ └── users.rs
│ │ └── routes/ # One per aggregate, mirrors db/
│ ├── time_tracking/
│ │ ├── guard.rs # require_time_tracking_enabled — returns 404 when disabled
│ │ ├── db.rs # record_time_entry (P1: double-credit on edit), payout transactions
│ │ ├── model.rs # TimeEntry, OvertimeBalance, PayoutRecord
│ │ └── routes.rs
│ ├── forms/
│ │ ├── db.rs # CRUD + customer_forms_lookup_by_slug call
│ │ ├── model.rs # CustomerForm, FormSubmission, EventDocument
│ │ ├── rate_limit.rs # Process-local DashMap RateLimiter (P2: per-tenant override, Redis)
│ │ ├── routes.rs # Admin + customer-facing forms; presigned S3 upload URL
│ │ └── validation.rs # Field-against-spec validation
│ ├── notifications/
│ │ ├── db.rs
│ │ ├── model.rs # Notification, PushSubscription, NotificationPayload
│ │ ├── push.rs # web-push payload encryption + dispatch
│ │ ├── routes.rs # In-app inbox + push subscription CRUD
│ │ └── service.rs # create_and_dispatch — call site fan-out (P1: not yet wired into business logic)
│ ├── events/ # Event-document-only routes (lives separately for tag grouping)
│ │ └── documents.rs
│ └── test_support.rs # #[cfg(test)] helpers
│
├── frontend/ # Part: frontend (project_type_id=web; Rust/Leptos WASM)
│ ├── Cargo.toml # edition 2024, leptos 0.8 nightly, wasm-bindgen, gloo-*, rust-ui crates
│ ├── rust-toolchain.toml # Pinned nightly-2026-04-25 + wasm32 target
│ ├── Trunk.toml # tailwindcss = "4.1.7", proxy /api/* → 127.0.0.1:3010, port 8080
│ ├── index.html # Trunk asset pipeline entry
│ ├── style/tailwind.css # Tailwind v4 input
│ ├── public/ # Static assets — manifest, icons, service worker target
│ └── src/
│ ├── main.rs # wasm-bindgen entry
│ ├── lib.rs # Re-exports
│ ├── app.rs # ENTRY — App component, Router with all routes
│ ├── api/
│ │ ├── client.rs # api_get/post/patch/delete — JWT bearer, /api/v1 base
│ │ └── types.rs # All DTOs mirrored from backend
│ ├── auth/
│ │ └── context.rs # provide_auth_context, get_token (from gloo-storage)
│ ├── ui/ # rust-ui primitives + our Sidebar (MIT, see LICENSE.rust-ui)
│ ├── admin/ # Surface 1 — /admin/* (10 routed pages)
│ │ ├── shell.rs # AdminShell layout
│ │ ├── dashboard.rs, locations.rs, roles.rs, settings.rs
│ │ ├── employees/ # list.rs, detail.rs
│ │ ├── events/ # list.rs, detail.rs
│ │ ├── forms/ # list.rs, builder.rs, submissions.rs
│ │ └── shifts/ # list.rs, calendar.rs (placeholder), detail.rs
│ ├── employee/ # Surface 2 — /app/* (5 routed pages)
│ │ ├── shell.rs
│ │ ├── schedule.rs, available.rs, overtime.rs, notifications.rs
│ └── customer/ # Surface 3 — /f/:slug (single route)
│ ├── form_page.rs
│ ├── form_renderer.rs
│ └── fields/ # text.rs, number.rs, date_field.rs, select.rs, location.rs
│
├── deploy/
│ ├── docker/
│ │ ├── backend.Dockerfile # Distroless final image
│ │ ├── nginx.Dockerfile # Alpine nginx serving frontend/dist
│ │ ├── nginx.conf
│ │ └── docker-compose.yml # backend + nginx + anubis + zitadel + rustfs (P1: pin :latest tags)
│ └── license/ # Standalone crate (workspace-excluded), ed25519 license signing CLI
│ ├── Cargo.toml
│ ├── Cargo.lock # Its own lockfile
│ └── src/
│
├── docs/ # ← you are here. Project documentation root.
│ ├── follow-ups.md # Outstanding work, prioritised
│ ├── licensing.md
│ ├── deployment/ # binary.md, docker-compose.md, nginx.md, zitadel-setup.md
│ └── superpowers/ # Original (frozen) plans + design spec
│ ├── plans/ # 01..09 implementation plans
│ └── specs/2026-05-08-shiftit-design.md
│
└── scripts/
├── run-backend.sh # cargo run with DEV_AUTH_* env for admin/employee/customer
├── mock-jwks.py # Tiny HTTP server: serves empty {"keys":[]} at /oauth/v2/keys
└── seed-demo.sql # Idempotent demo data (Demo GmbH tenant, 3 users, roles, locations, etc.)
Critical entry points
| Concern | File |
|---|---|
Backend bootstrap |
|
Backend route registration |
|
Backend OpenAPI catalog |
|
Frontend bootstrap |
|
Frontend route registration |
|
Auth middleware |
|
RLS tenant scope |
|
Migrations |
|
Multi-part interface
The two parts communicate exclusively over HTTP at /api/v1. There is no shared Rust code between them — the frontend redeclares DTO shapes in frontend/src/api/types.rs (intentionally minimal subsets, sometimes with #[serde(default)] for forward-compat). The interface is documented in integration-architecture.md.