Mercury/docs/superpowers/specs/2026-06-16-mercury-design.md
Matthew L McPeak 616bdd1206
All checks were successful
ci / build-ui (push) Successful in 14s
ci / test (push) Successful in 2m59s
ci / publish (push) Successful in 2m27s
Initial Commit
2026-06-17 20:13:02 -04:00

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 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)

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 buildui/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