- TypeScript 91.5%
- HTML 5.5%
- Makefile 1.7%
- Shell 1.2%
- Dockerfile 0.1%
unimo-gateway
Privacy-preserving gateway for search, LLM proxy, user management, and encrypted file sync. Currently implements search routing through Brave, Serper, and Google Suggest without exposing user identity. Semantic caching via embeddings + LSH. Domain-prefixed WebSocket protocol. Runs on Bun + Valkey (Docker). Optional SGX attestation.
Setup
git clone git@codeberg.org:thinking_tools/secure.gateway.unimo.git && cd secure.gateway.unimo
cp .env.example .env
Edit .env:
BRAVE_API_KEYS=key1 key2 # space-separated, round-robin rotation
SERPER_API_KEYS=key1 key2
REDIS_URL=redis://token@host:port # optional, for non-Docker setups
VALKEY_PASSWORD= # optional, Docker Valkey auth
PORT=3000
Get API keys: Brave Search | Serper
Run
Local (Bun)
bun install
bun run dev
REDIS_URL is optional — the app falls back to no caching without it.
Docker
See Docker section below.
Usage
Protected routes use PQ-issued session tokens: register an account, then POST /api/auth/login to mint a token, sent as Authorization: Bearer <token> with x-vault-id and x-member-id headers (the WebSocket upgrade accepts ?token=&vid=&mid= query params instead, since browsers can't set headers on WebSocket). GET / is public; admin routes require GATEWAY_ADMIN_TOKEN.
WebSocket — /api/search/ws
Connect and interact in real-time:
const ws = new WebSocket('ws://localhost:3000/api/search/ws?token=your-secret');
ws.onopen = () => {
// Get suggestions as you type
ws.send(JSON.stringify({ type: 'search:typing', id: '1', query: 'hello w' }));
// Full search
ws.send(JSON.stringify({ type: 'search:query', id: '2', query: 'hello world' }));
};
ws.onmessage = ({ data }) => {
const msg = JSON.parse(data);
// msg.type: 'ready' | 'search:suggestions' | 'search:results' | 'error'
};
Messages
| Direction | Type | Payload |
|---|---|---|
client → |
hello |
{ type, id? } |
client → |
search:typing |
{ type, id, query } |
client → |
search:query |
{ type, id, query, params? } |
← server |
ready |
{ type, version, commit, buildTime } |
← server |
search:suggestions |
{ type, id, suggestions: string[] } |
← server |
search:results |
{ type, id, data: UnifiedSearchResponse, cached } |
← server |
error |
{ type, id, error: string } |
REST
# Health check
curl http://localhost:3000/
# SGX attestation (when running in enclave)
curl http://localhost:3000/api/attestation/quote?token=your-secret
curl http://localhost:3000/api/attestation/mrenclave?token=your-secret
Docker
Both setups use Valkey as the Redis-compatible cache layer with AOF persistence enabled (appendfsync everysec), so data survives container restarts. REDIS_URL is auto-configured by compose — no need to set it in .env. Set VALKEY_PASSWORD in .env to enable Valkey authentication.
Docker files live in docker/ — all commands use --project-directory . so relative paths resolve from the repo root.
Dev
docker compose --project-directory . -f docker/compose.yaml -f docker/compose.override.yaml up
Auto-loads dev overrides — source-mounted with hot reload, low resource caps (768MB / 2 CPU per service). Installs deps and generates a fallback version.ts inside the container automatically. No npm install needed on the host.
Valkey is exposed on localhost:6379 for local debugging.
Production
# Build (pass current commit hash)
docker compose --project-directory . -f docker/compose.yaml -f docker/compose.prod.yaml build \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD)
# Run
docker compose --project-directory . -f docker/compose.yaml -f docker/compose.prod.yaml up -d
# Logs
docker compose --project-directory . -f docker/compose.yaml -f docker/compose.prod.yaml logs -f
# Stop
docker compose --project-directory . -f docker/compose.yaml -f docker/compose.prod.yaml down
Builds a multi-stage image (production deps only, non-root user). Valkey runs with 28GB maxmemory, AOF persistence (everysec), 65k file descriptors, and allkeys-lru eviction. Gateway has HTTP healthchecks on / and JSON log rotation.
Test
bun test
Architecture
Client ──WebSocket──▶ Hono App ──▶ MessageRouter ──▶ Domain Handlers
├── system: hello/ready
└── search: SearchEngine
├── Google Suggest (free, fast)
├── Serper (backfill + preload)
├── Brave Search (full results)
└── Semantic Cache (Redis + LSH embeddings)
- Suggest flow (
search:typing): exact cache → Google Suggest → background Serper preload - Search flow (
search:query): semantic cache lookup → parallel Brave search + suggestions → background cache write - Embeddings:
embeddinggemma-300mvia HuggingFace ONNX - Caching: 256-dim truncated embeddings, 12-table LSH, cosine similarity ≥ 0.82, 3-day TTL