10 KiB
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<String, (CachedQuery, Instant)>)
↓ 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 migrationsDashMap— concurrent hashmap for the query cacheglob— compiled pattern matching for the route blacklistjsonwebtoken— JWT encode/decode- Vue 3 + Vite +
@nychthemeron/library— frontend, served as static files by Rust tower-http::ServeDir— servesui/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<String, (CachedQuery, Instant)>
- Key: deterministic string derived from
{METHOD}:{table}:{params}for auto-generated queries, or queryidentifierfor named queries - Value: the compiled query + timestamp of last access
- Hit/miss counters: two
AtomicU64values 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<RwLock<Vec<(BlacklistEntry, glob::Pattern)>>>
- Loaded from DB at startup
- Reloaded (write lock, full rebuild from DB) on any admin write to the
blacklisttable - Checked in middleware on every request before routing: if any active entry matches
(method, path), return403 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_SECRETenv var - Passed as
Authorization: Bearer <token>header - Token expiry: configurable via
JWT_EXPIRY_SECS
Configuration (env vars)
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<RwLock<...>> + 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:
- Stage 1 (node): install
@nychthemeron/library, runvite build→ui/dist/ - Stage 2 (rust): compile API, copy
ui/dist/into final image - Final image runs the Rust binary;
tower-http::ServeDirservesui/dist/at/
docker-compose.yml:
dbservice:postgres:16-alpine, persistent volume, health checkapiservice: built from Dockerfile, depends ondb,mem_limitset, env vars from.env- Migrations run at API startup via
sqlx::migrate!()
Frontend Contract
- Dark mode is the default; set via
@nychthemeron/librarytheme config inmain.ts - Login page at
/logincallsPOST /auth/login, stores JWT inlocalStorage - All admin views require
SUPER_ADMINbit in JWT; route guard redirects to/loginif absent - Permission bitmask helpers in
stores/auth.ts:hasPermission(bit: number): boolean - Admin views wire directly to their respective admin endpoints