# Mercury — Design Spec **Date:** 2026-06-16 ## Overview Mercury is a monolithic, high-performance universal CRUD API written in Rust. It accepts HTTP requests, maps them to PostgreSQL tables, generates and caches SQL dynamically, and exposes an admin suite (with a Vue 3 frontend) for managing queries, users, permissions, and route security. A JWT-based bitmask permission system gates all admin operations. --- ## Architecture ``` HTTP Request ↓ Blacklist Middleware (in-memory glob match) ↓ Auth Middleware (JWT decode + permission bit check, admin routes only) ↓ Axum Router ↓ Query Cache (DashMap) ↓ cache miss Query Registry (PostgreSQL: queries table) ↓ query not yet registered Query Builder (auto-generates SQL from route + HTTP method) ↓ PostgreSQL (sqlx PgPool) ``` **Stack:** - `axum` — async HTTP framework (tokio-native) - `sqlx` — async PostgreSQL driver, raw query support, compile-time checked migrations - `DashMap` — concurrent hashmap for the query cache - `glob` — compiled pattern matching for the route blacklist - `jsonwebtoken` — JWT encode/decode - Vue 3 + Vite + `@nychthemeron/library` — frontend, served as static files by Rust - `tower-http::ServeDir` — serves `ui/dist/` at `/` --- ## Endpoints ### Public ``` POST /auth/login → returns JWT ``` ### CRUD (no auth, blacklist-checked) ``` GET /api/{table} → SELECT * FROM {table} [?col=val filters] GET /api/{table}/{id} → SELECT * FROM {table} WHERE id = {id} POST /api/{table} → INSERT INTO {table} PUT /api/{table}/{id} → UPDATE {table} WHERE id = {id} DELETE /api/{table}/{id} → DELETE FROM {table} WHERE id = {id} ``` `users` and `permissions` are blacklisted by default — only accessible via the admin suite. ### Admin — Query Registry (JWT + `ADMIN_QUERY` bit) ``` GET /admin/queries → list all registered queries POST /admin/queries → register raw SQL template GET /admin/queries/{identifier} → get query by slug identifier PUT /admin/queries/{identifier} → update SQL template or metadata DELETE /admin/queries/{identifier} → remove from registry + evict from cache GET /admin/queries/{identifier}/execute → execute query with ?param=val bindings ``` `{identifier}` is the human-readable slug (e.g. `get-user-orders`), not the internal UUID. The UUID is used internally only. ### Admin — Cache (JWT + `ADMIN_CACHE` bit) ``` GET /admin/cache/stats → current size, hit count, miss count DELETE /admin/cache → flush entire cache ``` ### Admin — Users (JWT + `SUPER_ADMIN` bit) ``` GET /admin/users → list users POST /admin/users → create user GET /admin/users/{id} → get user PUT /admin/users/{id} → update user DELETE /admin/users/{id} → delete user POST /admin/users/{id}/permissions/grant/{bit_value} → OR bit into mask DELETE /admin/users/{id}/permissions/revoke/{bit_value} → AND NOT bit from mask ``` ### Admin — Permissions (JWT + `SUPER_ADMIN` bit) ``` GET /admin/permissions → list all permission definitions POST /admin/permissions → create custom permission (auto-assigns next available bit_value) PUT /admin/permissions/{id} → update name/description DELETE /admin/permissions/{id} → remove permission definition ``` ### Admin — Blacklist (JWT + `SUPER_ADMIN` bit) ``` GET /admin/blacklist → list all entries POST /admin/blacklist → add route pattern PUT /admin/blacklist/{id} → update entry DELETE /admin/blacklist/{id} → remove entry ``` Any write to the blacklist immediately reloads the in-memory blacklist from the DB. ### Frontend (served by Rust) ``` GET / → serves ui/dist/index.html (SPA entry) GET /assets/* → static assets ``` --- ## Database Schema ### `users` | column | type | notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | username | VARCHAR UNIQUE NOT NULL | | | password_hash | TEXT NOT NULL | bcrypt | | permissions_mask | NUMERIC NOT NULL DEFAULT 0 | u128 bitmask | | created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | | ### `permissions` | column | type | notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | name | VARCHAR UNIQUE NOT NULL | e.g. `READ` | | bit_value | NUMERIC UNIQUE NOT NULL | power of 2, up to 2^127 | | description | TEXT | | ### `queries` | column | type | notes | |---|---|---| | id | UUID PRIMARY KEY DEFAULT gen_random_uuid() | | | identifier | VARCHAR UNIQUE NOT NULL | human-readable slug | | sql_template | TEXT NOT NULL | raw SQL with :param placeholders | | description | TEXT | | | created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | | ### `blacklist` | column | type | notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | pattern | VARCHAR NOT NULL | glob-style, e.g. `/api/users/**` | | method | VARCHAR | NULL = all methods | | reason | TEXT | | | active | BOOLEAN NOT NULL DEFAULT true | | | created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | | ### Seed Data (migration) Default permission definitions: | name | bit_value | |---|---| | READ | 1 | | WRITE | 2 | | DELETE | 4 | | ADMIN_QUERY | 8 | | ADMIN_CACHE | 16 | | SUPER_ADMIN | 32 | Default user: `admin` / `admin`, `permissions_mask = 63` (all bits set). Default blacklist entries: `/api/users/**` and `/api/permissions/**` (method: NULL). --- ## Query Cache **Structure:** `DashMap` - Key: deterministic string derived from `{METHOD}:{table}:{params}` for auto-generated queries, or query `identifier` for named queries - Value: the compiled query + timestamp of last access - Hit/miss counters: two `AtomicU64` values on the shared state struct **Eviction:** a tokio background task runs every `CACHE_SWEEP_INTERVAL_SECS`. It iterates the map and removes entries where `last_accessed.elapsed() > CACHE_IDLE_TIMEOUT_SECS`. **Capacity:** before inserting a new entry, if `cache.len() >= CACHE_MAX_CAPACITY`, the entry with the oldest `last_accessed` is evicted first. **Cache invalidation:** deleting or updating a query via the admin suite immediately removes it from the DashMap. --- ## Blacklist Cache **Structure:** `Arc>>` - Loaded from DB at startup - Reloaded (write lock, full rebuild from DB) on any admin write to the `blacklist` table - Checked in middleware on every request before routing: if any active entry matches `(method, path)`, return `403 Forbidden` --- ## Permission System Permissions are u128 bitmasks. The JWT claims embed the user's current `permissions_mask`. Middleware extracts the JWT and performs a bitwise AND check — no DB lookup per request. ``` READ = 1 WRITE = 2 DELETE = 4 ADMIN_QUERY = 8 ADMIN_CACHE = 16 SUPER_ADMIN = 32 ``` Custom permissions auto-assign the next unused power-of-2 bit_value. Up to 128 distinct permission bits supported. `permissions_mask` stored as PostgreSQL `NUMERIC` (arbitrary precision), converted to/from `u128` in the Rust data layer. --- ## JWT - Algorithm: HS256 - Claims: `sub` (username), `permissions` (u128 as string), `exp` - Secret: `JWT_SECRET` env var - Passed as `Authorization: Bearer ` header - Token expiry: configurable via `JWT_EXPIRY_SECS` --- ## Configuration (env vars) ```env DATABASE_URL=postgres://mercury:mercury@db:5432/mercury JWT_SECRET=changeme JWT_EXPIRY_SECS=3600 CACHE_MAX_CAPACITY=10000 CACHE_IDLE_TIMEOUT_SECS=300 CACHE_SWEEP_INTERVAL_SECS=60 ``` --- ## Project Structure ``` Mercury/ src/ main.rs -- startup: config, DB pool, cache init, router mount config.rs -- Config struct loaded from env auth/ mod.rs -- JWT encode/decode, Claims struct middleware.rs -- axum middleware: validate JWT, check permission bit cache/ query_cache.rs -- DashMap cache + sweep task + hit/miss counters blacklist_cache.rs -- Arc> + glob::Pattern, reload fn db/ pool.rs -- sqlx PgPool init migrations/ -- .sql files, run via sqlx::migrate!() 001_initial.sql -- all tables + seed data routes/ crud.rs -- generic /api/{table} handler + query builder auth.rs -- /auth/login admin/ queries.rs cache.rs users.rs permissions.rs blacklist.rs models/ query.rs user.rs permission.rs blacklist.rs ui/ .npmrc -- @nychthemeron:registry=https://git.mcpeakdev.com/api/packages/McPeakDev/npm/ package.json vite.config.ts src/ main.ts -- dark mode default, router, pinia init App.vue router/index.ts -- route guards: redirect /login if no JWT stores/auth.ts -- JWT storage, permissions bitmask helpers views/ Login.vue admin/ Queries.vue Users.vue Permissions.vue Blacklist.vue Cache.vue Cargo.toml docker-compose.yml Dockerfile -- multi-stage: Node build UI, Rust build API .env.example ``` --- ## Docker **Multi-stage Dockerfile:** 1. Stage 1 (node): install `@nychthemeron/library`, run `vite build` → `ui/dist/` 2. Stage 2 (rust): compile API, copy `ui/dist/` into final image 3. Final image runs the Rust binary; `tower-http::ServeDir` serves `ui/dist/` at `/` **docker-compose.yml:** - `db` service: `postgres:16-alpine`, persistent volume, health check - `api` service: built from Dockerfile, depends on `db`, `mem_limit` set, env vars from `.env` - Migrations run at API startup via `sqlx::migrate!()` --- ## Frontend Contract - Dark mode is the default; set via `@nychthemeron/library` theme config in `main.ts` - Login page at `/login` calls `POST /auth/login`, stores JWT in `localStorage` - All admin views require `SUPER_ADMIN` bit in JWT; route guard redirects to `/login` if absent - Permission bitmask helpers in `stores/auth.ts`: `hasPermission(bit: number): boolean` - Admin views wire directly to their respective admin endpoints