Mercury/docs/superpowers/specs/2026-06-16-mercury-design.md
Matthew L McPeak dd700d02ad
All checks were successful
ci / build-ui (push) Successful in 14s
ci / test (push) Successful in 3m50s
ci / publish (push) Successful in 3m9s
Inital Commit
2026-07-16 12:36:14 -04:00

323 lines
10 KiB
Markdown

# 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 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<String, (CachedQuery, Instant)>`
- 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<RwLock<Vec<(BlacklistEntry, glob::Pattern)>>>`
- 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 <token>` 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<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:**
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