feat: Introduce mobile simulator UI with new routes, screens, shared application chrome, and updated dependencies.

This commit is contained in:
2026-02-26 13:45:00 +00:00
parent 50760ae664
commit 1ee6b21808
22 changed files with 3727 additions and 5 deletions

View File

@@ -0,0 +1,934 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Backend Architecture Deep Dive</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #071015;
--panel: #0e1b22;
--panel-2: #132731;
--text: #dbe7ef;
--muted: #94adbb;
--line: #255061;
--accent: #22c55e;
--accent-2: #06b6d4;
--warn: #f59e0b;
--danger: #f97316;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 0% 0%, #113141 0%, transparent 45%),
radial-gradient(circle at 100% 0%, #17303a 0%, transparent 45%),
var(--bg);
line-height: 1.5;
}
.wrap {
max-width: 1320px;
margin: 0 auto;
padding: 24px;
}
.hero {
border: 1px solid var(--line);
background: linear-gradient(165deg, #122734 0%, #0b171e 60%);
border-radius: 18px;
padding: 24px;
margin-bottom: 18px;
}
h1, h2, h3 {
margin: 0 0 10px 0;
line-height: 1.25;
}
h1 {
font-size: clamp(1.5rem, 2vw, 2.2rem);
}
h2 {
font-size: clamp(1.2rem, 1.6vw, 1.6rem);
margin-top: 26px;
margin-bottom: 12px;
}
h3 {
font-size: 1rem;
color: #d2effb;
margin-top: 12px;
}
p, li {
color: var(--muted);
}
strong, b {
color: #eaf7ff;
}
code, .mono {
font-family: "IBM Plex Mono", monospace;
font-size: 0.9em;
background: #0a151b;
border: 1px solid #1b3744;
padding: 2px 6px;
border-radius: 6px;
color: #b7f5ff;
}
.small {
font-size: 0.88rem;
color: #8aa1af;
}
.grid {
display: grid;
gap: 12px;
}
.g2 {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.g3 {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.card {
background: linear-gradient(180deg, var(--panel-2), var(--panel));
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
}
.chip {
display: inline-block;
border: 1px solid #2e6074;
color: #9ed4ea;
border-radius: 999px;
padding: 2px 10px;
font-size: 0.8rem;
margin: 0 4px 6px 0;
background: #10232d;
}
.chip.warn {
border-color: #7a5b1c;
color: #ffd58a;
background: #20190c;
}
.chip.ok {
border-color: #1f6a42;
color: #8df0bc;
background: #0b1d15;
}
.chip.alt {
border-color: #23617b;
color: #8cd7f8;
background: #0d1f29;
}
.svg-box {
border: 1px solid var(--line);
border-radius: 14px;
background: #0a161d;
padding: 10px;
overflow: auto;
}
svg {
width: 100%;
height: auto;
min-width: 840px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
background: #0b171e;
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
}
th, td {
padding: 9px 10px;
border-bottom: 1px solid #1f3c4b;
vertical-align: top;
text-align: left;
}
th {
background: #132833;
color: #d9f4ff;
font-weight: 600;
}
tr:last-child td {
border-bottom: none;
}
details {
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px 12px;
background: #0b171e;
margin-bottom: 10px;
}
summary {
cursor: pointer;
color: #cde9f8;
font-weight: 600;
}
.seq {
white-space: pre;
overflow: auto;
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px;
background: #081117;
color: #b4d4e2;
font-family: "IBM Plex Mono", monospace;
font-size: 0.82rem;
line-height: 1.45;
}
.toc {
position: sticky;
top: 10px;
border: 1px solid var(--line);
border-radius: 12px;
background: #0a161d;
padding: 10px;
}
.toc a {
color: #9dc9dd;
text-decoration: none;
display: block;
padding: 3px 2px;
font-size: 0.9rem;
}
.toc a:hover {
color: #d4f3ff;
}
.layout {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 16px;
align-items: start;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
}
.toc {
position: static;
}
svg {
min-width: 760px;
}
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<div class="small">SecureCam Backend Architecture</div>
<h1>Backend Architecture Deep Dive (Single-Page Reference)</h1>
<p>
This document explains how the backend is built, how requests and realtime events move through the system,
how data is stored, and where each concern lives in code.
It is based on the current implementation in this repository (<span class="mono">index.ts</span>,
<span class="mono">routes/*</span>, <span class="mono">realtime/gateway.ts</span>,
<span class="mono">services/*</span>, <span class="mono">workers/*</span>, <span class="mono">db/schema.ts</span>).
</p>
<div>
<span class="chip ok">Express 5 API</span>
<span class="chip alt">Socket.IO Realtime</span>
<span class="chip">Better Auth + Drizzle</span>
<span class="chip">PostgreSQL</span>
<span class="chip">MinIO / S3-compatible</span>
<span class="chip warn">Mock Media Provider</span>
</div>
</section>
<div class="layout">
<aside class="toc">
<strong>Contents</strong>
<a href="#system">1. System Context</a>
<a href="#startup">2. Startup Sequence</a>
<a href="#pipeline">3. HTTP Pipeline</a>
<a href="#auth">4. Auth + Identity</a>
<a href="#realtime">5. Realtime Gateway</a>
<a href="#stream">6. Stream Lifecycle</a>
<a href="#data">7. Data Model</a>
<a href="#routes">8. Route Surface</a>
<a href="#workers">9. Workers + Reliability</a>
<a href="#security">10. Security Controls</a>
<a href="#config">11. Configuration</a>
<a href="#code-map">12. Code Ownership Map</a>
<a href="#constraints">13. Current Constraints</a>
</aside>
<main>
<section id="system">
<h2>1) System Context</h2>
<div class="svg-box">
<svg viewBox="0 0 1220 560" role="img" aria-label="System context architecture diagram">
<defs>
<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#6ec9e8" />
</marker>
</defs>
<rect x="20" y="20" width="280" height="170" rx="12" fill="#132833" stroke="#2d5d71"/>
<text x="36" y="52" fill="#d7f2ff" font-size="18" font-weight="600">Clients</text>
<text x="36" y="84" fill="#95bacb" font-size="15">- Browser simulator (camera/client)</text>
<text x="36" y="109" fill="#95bacb" font-size="15">- Future mobile apps</text>
<text x="36" y="134" fill="#95bacb" font-size="15">- Admin browser</text>
<text x="36" y="159" fill="#95bacb" font-size="15">- Swagger/OpenAPI consumers</text>
<rect x="20" y="230" width="280" height="140" rx="12" fill="#1f2a20" stroke="#3d7249"/>
<text x="36" y="262" fill="#dcffe5" font-size="18" font-weight="600">Identity</text>
<text x="36" y="292" fill="#a8d8b6" font-size="15">Better Auth session cookies</text>
<text x="36" y="317" fill="#a8d8b6" font-size="15">Custom HMAC device bearer tokens</text>
<text x="36" y="342" fill="#a8d8b6" font-size="15">Role-aware device auth</text>
<rect x="360" y="20" width="530" height="390" rx="14" fill="#0f1e27" stroke="#2f6176"/>
<text x="380" y="52" fill="#def6ff" font-size="20" font-weight="700">Backend Process (Bun + Express + Socket.IO)</text>
<rect x="380" y="70" width="235" height="120" rx="10" fill="#13303d" stroke="#2e647a"/>
<text x="396" y="98" fill="#d7f2ff" font-size="16" font-weight="600">HTTP Layer</text>
<text x="396" y="122" fill="#9fc8da" font-size="14">Helmet + CORS + rate limits</text>
<text x="396" y="143" fill="#9fc8da" font-size="14">requestContext metrics + logs</text>
<text x="396" y="164" fill="#9fc8da" font-size="14">REST routes + OpenAPI docs</text>
<rect x="640" y="70" width="230" height="120" rx="10" fill="#13303d" stroke="#2e647a"/>
<text x="658" y="98" fill="#d7f2ff" font-size="16" font-weight="600">Realtime Gateway</text>
<text x="658" y="122" fill="#9fc8da" font-size="14">Socket.IO device rooms</text>
<text x="658" y="143" fill="#9fc8da" font-size="14">command / stream / webrtc signals</text>
<text x="658" y="164" fill="#9fc8da" font-size="14">presence + retry loop</text>
<rect x="380" y="210" width="235" height="180" rx="10" fill="#1d2e23" stroke="#447551"/>
<text x="396" y="238" fill="#e3ffe8" font-size="16" font-weight="600">Service / Worker Layer</text>
<text x="396" y="262" fill="#b8dfc3" font-size="14">push queue worker</text>
<text x="396" y="282" fill="#b8dfc3" font-size="14">recording timeout reconciler</text>
<text x="396" y="302" fill="#b8dfc3" font-size="14">audit logging</text>
<text x="396" y="322" fill="#b8dfc3" font-size="14">health + metrics endpoints</text>
<text x="396" y="342" fill="#b8dfc3" font-size="14">media provider adapter</text>
<rect x="640" y="210" width="230" height="180" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="658" y="238" fill="#fff1da" font-size="16" font-weight="600">Media Control Plane</text>
<text x="658" y="262" fill="#f2d7aa" font-size="14">stream session orchestration</text>
<text x="658" y="282" fill="#f2d7aa" font-size="14">mock credential issuance</text>
<text x="658" y="302" fill="#f2d7aa" font-size="14">optional SFU scaffold (noop)</text>
<text x="658" y="322" fill="#f2d7aa" font-size="14">recording row lifecycle</text>
<rect x="940" y="40" width="260" height="160" rx="12" fill="#192b35" stroke="#39677d"/>
<text x="958" y="70" fill="#daf4ff" font-size="18" font-weight="600">PostgreSQL</text>
<text x="958" y="98" fill="#9fc5d8" font-size="14">users, devices, links, commands</text>
<text x="958" y="118" fill="#9fc5d8" font-size="14">streams, recordings, events</text>
<text x="958" y="138" fill="#9fc5d8" font-size="14">videos, notifications, audit</text>
<text x="958" y="158" fill="#9fc5d8" font-size="14">Better Auth tables</text>
<rect x="940" y="240" width="260" height="160" rx="12" fill="#2b2318" stroke="#7c6039"/>
<text x="958" y="270" fill="#fff0d4" font-size="18" font-weight="600">MinIO / Object Storage</text>
<text x="958" y="298" fill="#ebd3aa" font-size="14">presigned PUT / GET URLs</text>
<text x="958" y="318" fill="#ebd3aa" font-size="14">video objects + recordings</text>
<text x="958" y="338" fill="#ebd3aa" font-size="14">bucket bootstrap on startup</text>
<line x1="300" y1="108" x2="360" y2="108" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
<line x1="300" y1="298" x2="360" y2="138" stroke="#7ee2aa" stroke-width="2" marker-end="url(#arr)"/>
<line x1="890" y1="120" x2="940" y2="120" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
<line x1="890" y1="290" x2="940" y2="300" stroke="#f1bf73" stroke-width="2" marker-end="url(#arr)"/>
<line x1="700" y1="190" x2="700" y2="210" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
<line x1="500" y1="190" x2="500" y2="210" stroke="#6ec9e8" stroke-width="2" marker-end="url(#arr)"/>
</svg>
</div>
<div class="grid g3" style="margin-top:10px;">
<div class="card"><h3>Core Role</h3><p>Acts primarily as a <strong>control plane</strong> for auth, command routing, stream state, credential issuance, and recording metadata. It is not yet a full production media plane.</p></div>
<div class="card"><h3>Transport Split</h3><p><strong>HTTP</strong> handles CRUD/state endpoints; <strong>Socket.IO</strong> handles realtime command delivery, acknowledgements, and WebRTC signaling relay.</p></div>
<div class="card"><h3>Persistence Split</h3><p><strong>Postgres</strong> stores state + metadata. <strong>MinIO</strong> stores binary objects. Routes often coordinate both.</p></div>
</div>
</section>
<section id="startup">
<h2>2) Startup Sequence</h2>
<div class="seq">Process start
-> load module graph (auth, db, minio, routes)
-> create Express app + OpenAPI doc
-> mount middleware and routes
-> create HTTP server
-> start() called
-> ensureMinioBucket()
- checks bucket existence
- creates bucket if missing
- exits process on failure
-> setupRealtimeGateway(server)
- Socket.IO auth middleware
- event handlers
- command retry interval init (if required tables exist)
-> startRecordingsWorker()
- interval scans stale awaiting_upload rows -> failed
-> startPushWorker()
- interval dispatches queued push rows
-> server.listen(PORT)
</div>
<p class="small">If MinIO initialization fails, process exits with code 1 by design (<span class="mono">index.ts</span>).</p>
</section>
<section id="pipeline">
<h2>3) HTTP Request Pipeline (Express)</h2>
<div class="svg-box">
<svg viewBox="0 0 1220 360" role="img" aria-label="HTTP middleware pipeline diagram">
<defs>
<marker id="arr2" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#90ddff" />
</marker>
</defs>
<rect x="20" y="128" width="130" height="80" rx="10" fill="#122733" stroke="#2b5e74"/>
<text x="42" y="173" fill="#d6f2ff" font-size="16">Client</text>
<rect x="190" y="70" width="160" height="190" rx="10" fill="#132a35" stroke="#2f6074"/>
<text x="205" y="100" fill="#ddf6ff" font-size="15" font-weight="600">helmet()</text>
<text x="205" y="124" fill="#9ec5d6" font-size="13">CSP, headers</text>
<text x="205" y="154" fill="#ddf6ff" font-size="15" font-weight="600">cors()</text>
<text x="205" y="178" fill="#9ec5d6" font-size="13">trusted origins</text>
<text x="205" y="208" fill="#ddf6ff" font-size="15" font-weight="600">global rate limit</text>
<text x="205" y="232" fill="#9ec5d6" font-size="13">memory buckets</text>
<rect x="390" y="70" width="200" height="190" rx="10" fill="#132a35" stroke="#2f6074"/>
<text x="408" y="100" fill="#ddf6ff" font-size="15" font-weight="600">requestContext</text>
<text x="408" y="124" fill="#9ec5d6" font-size="13">x-request-id</text>
<text x="408" y="144" fill="#9ec5d6" font-size="13">counter increment</text>
<text x="408" y="164" fill="#9ec5d6" font-size="13">JSON finish log</text>
<text x="408" y="198" fill="#ddf6ff" font-size="15" font-weight="600">express.json()</text>
<text x="408" y="222" fill="#9ec5d6" font-size="13">body parsing</text>
<rect x="630" y="52" width="270" height="226" rx="10" fill="#1d2e23" stroke="#447551"/>
<text x="648" y="82" fill="#e6ffea" font-size="15" font-weight="600">Route layer</text>
<text x="648" y="106" fill="#badfc5" font-size="13">/api/auth/* (Better Auth)</text>
<text x="648" y="126" fill="#badfc5" font-size="13">/videos /devices /commands ...</text>
<text x="648" y="146" fill="#badfc5" font-size="13">route-level auth + zod validation</text>
<text x="648" y="166" fill="#badfc5" font-size="13">DB + MinIO + realtime side effects</text>
<text x="648" y="200" fill="#e6ffea" font-size="15" font-weight="600">Static + docs</text>
<text x="648" y="224" fill="#badfc5" font-size="13">/sim, /docs, /openapi.json</text>
<rect x="940" y="90" width="260" height="150" rx="10" fill="#2a2118" stroke="#7d613a"/>
<text x="958" y="120" fill="#fff2d9" font-size="15" font-weight="600">Response + Error Handler</text>
<text x="958" y="146" fill="#edd6ad" font-size="13">successful JSON / HTML / static file</text>
<text x="958" y="168" fill="#edd6ad" font-size="13">or fallback 500 JSON</text>
<text x="958" y="190" fill="#edd6ad" font-size="13">{ message: "Internal server error" }</text>
<line x1="150" y1="168" x2="190" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="350" y1="168" x2="390" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="590" y1="168" x2="630" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="900" y1="168" x2="940" y2="168" stroke="#90ddff" stroke-width="2" marker-end="url(#arr2)"/>
</svg>
</div>
</section>
<section id="auth">
<h2>4) Authentication and Identity Model</h2>
<div class="grid g2">
<div class="card">
<h3>A) Session auth (<span class="mono">requireAuth</span>)</h3>
<ul>
<li>Used by user-facing REST routes like <span class="mono">/videos</span>, <span class="mono">/devices/register</span>, <span class="mono">/device-links</span>.</li>
<li>Reads Better Auth session from request headers/cookies via <span class="mono">auth.api.getSession()</span>.</li>
<li>Attaches session object to <span class="mono">req.auth</span>.</li>
<li>Backed by Better Auth tables: <span class="mono">users</span>, <span class="mono">account</span>, <span class="mono">session</span>, <span class="mono">verification</span>.</li>
</ul>
</div>
<div class="card">
<h3>B) Device auth (<span class="mono">requireDeviceAuth</span>)</h3>
<ul>
<li>Used by device-to-backend routes and Socket.IO auth.</li>
<li>Bearer token format: <span class="mono">base64url(payload).hmac</span>.</li>
<li>Payload fields: <span class="mono">userId</span>, <span class="mono">deviceId</span>, <span class="mono">role</span>, <span class="mono">exp</span>.</li>
<li>Signed with HMAC-SHA256 using <span class="mono">BETTER_AUTH_SECRET</span>.</li>
<li>Token role is verified against device role in realtime handshake.</li>
</ul>
</div>
</div>
<p class="small">This dual model separates user session identity from per-device identity and permissions.</p>
</section>
<section id="realtime">
<h2>5) Realtime Gateway (Socket.IO)</h2>
<div class="grid g2">
<div class="card">
<h3>Connection model</h3>
<ul>
<li>Devices authenticate with token in <span class="mono">handshake.auth.token</span> or <span class="mono">Authorization</span> header.</li>
<li>Each device joins room <span class="mono">device:{deviceId}</span>.</li>
<li>Presence updates <span class="mono">devices.status</span> + <span class="mono">lastSeenAt</span>.</li>
<li>Disconnect applies a 500ms delay to reduce status flapping on fast reconnect.</li>
</ul>
</div>
<div class="card">
<h3>Gateway responsibilities</h3>
<ul>
<li><span class="mono">command:received</span> delivery to target room.</li>
<li><span class="mono">command:ack</span> validation + DB update + source notification.</li>
<li><span class="mono">webrtc:signal</span> relay with same-owner target validation.</li>
<li><span class="mono">stream:frame</span> relay fallback (base64 image snapshots).</li>
<li>Retry worker for stale sent commands every 5s, max 3 retries.</li>
</ul>
</div>
</div>
<h3>Command dispatch and ack sequence</h3>
<div class="seq">Client Device API /commands DB Socket.IO Gateway Camera Device
| | | | |
1) POST /commands -------->| validate/link ---->| insert queued command | |
| | dispatchCommandById() | |
| |-----------------------------------------------> emit command:received --->|
| | | status sent/queued | |
|<--------------------| command payload | | |
2) camera emits command:ack --------------------------------------------------------------->|
| | | | validate + update DB |
| | | command status + ack time | emit command:status -->| (to source room)
</div>
</section>
<section id="stream">
<h2>6) Stream Lifecycle and Media Control</h2>
<div class="card">
<h3>State machine (stream session)</h3>
<div class="seq">requested --> streaming --> completed|cancelled|failed
| | |
| | +-- create recording placeholder row on end
| +-- media session created (provider + endpoints)
+-- command start_stream queued/dispatched to camera
</div>
</div>
<h3>On-demand stream end-to-end sequence</h3>
<div class="seq">Client Device /streams/request Camera Device /streams/:id/accept Media Provider
| | | | |
1) request stream -------------->| validate roles/link -> | | |
| | create stream_session | | |
| | create start_stream cmd | | |
| | dispatch via socket ----+-------------------------> | command:received |
|<--------------------------| stream:requested event | | |
2) accept command (camera side)
| | | POST /accept -----------> | createSession() ------>|
| | | | status=streaming |
|<--------------------------| stream:started event ---+ | |
3) credential issuance
camera -> GET /publish-credentials -> mediaProvider.issuePublishCredentials()
viewer -> GET /subscribe-credentials -> mediaProvider.issueSubscribeCredentials()
4) stream end
camera/requester -> POST /end -> mark ended + optional sfuService.endSession() + createRecordingForStream()
-> emit stream:ended to camera and requester (push fallback if offline)
</div>
<div class="grid g2">
<div class="card">
<h3>Media provider abstraction</h3>
<ul>
<li>Current provider: <span class="mono">mock</span> (<span class="mono">media/providers/mock.ts</span>).</li>
<li>Creates deterministic mock media session IDs.</li>
<li>Issues signed publish/subscribe tokens with TTL.</li>
<li>Uses <span class="mono">BETTER_AUTH_SECRET</span> for HMAC signing.</li>
</ul>
</div>
<div class="card">
<h3>SFU mode status</h3>
<ul>
<li><span class="mono">MEDIA_MODE=single_server_sfu</span> enables SFU endpoints.</li>
<li>Current implementation is a <strong>noop scaffold</strong> with in-memory session registry + synthetic transport IDs.</li>
<li>No full server-side RTP forwarding pipeline implemented yet.</li>
</ul>
</div>
</div>
</section>
<section id="data">
<h2>7) Data Model (Core Tables and Relationships)</h2>
<div class="svg-box">
<svg viewBox="0 0 1280 880" role="img" aria-label="ER-style architecture data diagram">
<defs>
<marker id="arr3" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<polygon points="0 0, 8 4, 0 8" fill="#8bcbe3" />
</marker>
</defs>
<rect x="30" y="30" width="260" height="170" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="48" y="58" fill="#ddf6ff" font-size="17" font-weight="700">users</text>
<text x="48" y="82" fill="#a5c9da" font-size="13">id (PK), email, name</text>
<text x="48" y="102" fill="#a5c9da" font-size="13">passwordHash, emailVerified</text>
<text x="48" y="122" fill="#a5c9da" font-size="13">createdAt, updatedAt</text>
<rect x="340" y="30" width="290" height="210" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="358" y="58" fill="#ddf6ff" font-size="17" font-weight="700">devices</text>
<text x="358" y="82" fill="#a5c9da" font-size="13">id (PK), userId (FK -> users)</text>
<text x="358" y="102" fill="#a5c9da" font-size="13">role, status, isCamera</text>
<text x="358" y="122" fill="#a5c9da" font-size="13">platform, appVersion, pushToken</text>
<text x="358" y="142" fill="#a5c9da" font-size="13">lastSeenAt, timestamps</text>
<rect x="680" y="30" width="300" height="190" rx="10" fill="#153329" stroke="#3f7351"/>
<text x="698" y="58" fill="#e4ffea" font-size="17" font-weight="700">device_links</text>
<text x="698" y="82" fill="#b8dfc4" font-size="13">ownerUserId -> users</text>
<text x="698" y="102" fill="#b8dfc4" font-size="13">cameraDeviceId -> devices</text>
<text x="698" y="122" fill="#b8dfc4" font-size="13">clientDeviceId -> devices</text>
<text x="698" y="142" fill="#b8dfc4" font-size="13">unique(cameraDeviceId, clientDeviceId)</text>
<rect x="1020" y="30" width="230" height="210" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="1038" y="58" fill="#fff1da" font-size="17" font-weight="700">device_commands</text>
<text x="1038" y="82" fill="#f0d7ac" font-size="13">sourceDeviceId -> devices</text>
<text x="1038" y="102" fill="#f0d7ac" font-size="13">targetDeviceId -> devices</text>
<text x="1038" y="122" fill="#f0d7ac" font-size="13">commandType, payload</text>
<text x="1038" y="142" fill="#f0d7ac" font-size="13">status, retryCount, ackAt</text>
<rect x="340" y="300" width="300" height="210" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="358" y="328" fill="#ddf6ff" font-size="17" font-weight="700">stream_sessions</text>
<text x="358" y="352" fill="#a5c9da" font-size="13">ownerUserId -> users</text>
<text x="358" y="372" fill="#a5c9da" font-size="13">cameraDeviceId -> devices</text>
<text x="358" y="392" fill="#a5c9da" font-size="13">requesterDeviceId -> devices</text>
<text x="358" y="412" fill="#a5c9da" font-size="13">status, reason, mediaProvider</text>
<text x="358" y="432" fill="#a5c9da" font-size="13">mediaSessionId, streamKey</text>
<rect x="680" y="300" width="300" height="210" rx="10" fill="#153329" stroke="#3f7351"/>
<text x="698" y="328" fill="#e4ffea" font-size="17" font-weight="700">recordings</text>
<text x="698" y="352" fill="#b8dfc4" font-size="13">streamSessionId -> stream_sessions</text>
<text x="698" y="372" fill="#b8dfc4" font-size="13">cameraDeviceId, requesterDeviceId</text>
<text x="698" y="392" fill="#b8dfc4" font-size="13">status awaiting_upload/ready/failed</text>
<text x="698" y="412" fill="#b8dfc4" font-size="13">objectKey, bucket, duration, size</text>
<rect x="1020" y="300" width="230" height="190" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="1038" y="328" fill="#fff1da" font-size="17" font-weight="700">events</text>
<text x="1038" y="352" fill="#f0d7ac" font-size="13">userId -> users</text>
<text x="1038" y="372" fill="#f0d7ac" font-size="13">deviceId -> devices</text>
<text x="1038" y="392" fill="#f0d7ac" font-size="13">startedAt, endedAt, status</text>
<text x="1038" y="412" fill="#f0d7ac" font-size="13">triggeredBy, videoUrl</text>
<rect x="30" y="560" width="300" height="220" rx="10" fill="#132833" stroke="#2f6074"/>
<text x="48" y="588" fill="#ddf6ff" font-size="17" font-weight="700">videos (legacy upload metadata)</text>
<text x="48" y="612" fill="#a5c9da" font-size="13">userId, deviceId, eventId</text>
<text x="48" y="632" fill="#a5c9da" font-size="13">objectKey, bucket, uploadUrl</text>
<text x="48" y="652" fill="#a5c9da" font-size="13">downloadUrl, status, expiresAt</text>
<rect x="370" y="560" width="300" height="220" rx="10" fill="#153329" stroke="#3f7351"/>
<text x="388" y="588" fill="#e4ffea" font-size="17" font-weight="700">push_notifications</text>
<text x="388" y="612" fill="#b8dfc4" font-size="13">ownerUserId -> users</text>
<text x="388" y="632" fill="#b8dfc4" font-size="13">recipientDeviceId -> devices</text>
<text x="388" y="652" fill="#b8dfc4" font-size="13">type, payload, status</text>
<text x="388" y="672" fill="#b8dfc4" font-size="13">attempts, nextAttemptAt, sentAt</text>
<rect x="710" y="560" width="270" height="220" rx="10" fill="#2f271d" stroke="#7a5c2f"/>
<text x="728" y="588" fill="#fff1da" font-size="17" font-weight="700">audit_logs</text>
<text x="728" y="612" fill="#f0d7ac" font-size="13">ownerUserId -> users</text>
<text x="728" y="632" fill="#f0d7ac" font-size="13">actorDeviceId -> devices</text>
<text x="728" y="652" fill="#f0d7ac" font-size="13">action, targetType, targetId</text>
<text x="728" y="672" fill="#f0d7ac" font-size="13">metadata, ipAddress, createdAt</text>
<rect x="1020" y="560" width="230" height="220" rx="10" fill="#1f2d36" stroke="#4a6f80"/>
<text x="1038" y="588" fill="#daf4ff" font-size="17" font-weight="700">Better Auth tables</text>
<text x="1038" y="612" fill="#a9cfdf" font-size="13">account</text>
<text x="1038" y="632" fill="#a9cfdf" font-size="13">session</text>
<text x="1038" y="652" fill="#a9cfdf" font-size="13">verification</text>
<line x1="290" y1="96" x2="340" y2="96" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="290" y1="130" x2="680" y2="130" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="630" y1="140" x2="1020" y2="140" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="490" y1="240" x2="490" y2="300" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="640" y1="404" x2="680" y2="404" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="1135" y1="240" x2="1135" y2="300" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="190" y1="200" x2="190" y2="560" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="490" y1="510" x2="520" y2="560" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
<line x1="490" y1="510" x2="845" y2="560" stroke="#8bcbe3" stroke-width="2" marker-end="url(#arr3)"/>
</svg>
</div>
<p class="small">Note: <span class="mono">notifications</span> table exists for event notification tracking; push delivery queue is modeled separately by <span class="mono">push_notifications</span>.</p>
</section>
<section id="routes">
<h2>8) Route Surface and Responsibilities</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Auth</th>
<th>Main tables/resources</th>
<th>Side effects</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="mono">/api/auth/*</span></td>
<td>Better Auth</td>
<td>users, account, session, verification</td>
<td>session cookie lifecycle</td>
</tr>
<tr>
<td><span class="mono">/devices</span></td>
<td>session + device token for heartbeat</td>
<td>devices, device_links</td>
<td>auto-link opposite-role devices on register; stale-status projection on list</td>
</tr>
<tr>
<td><span class="mono">/device-links</span></td>
<td>session</td>
<td>device_links, devices</td>
<td>enforces camera/client role pairing</td>
</tr>
<tr>
<td><span class="mono">/commands</span></td>
<td>session + device token ack fallback</td>
<td>device_commands, devices, device_links</td>
<td>dispatch to Socket.IO, ack/reject propagation</td>
</tr>
<tr>
<td><span class="mono">/events</span></td>
<td>device token (start/end), session (list)</td>
<td>events, device_links, notifications</td>
<td>realtime motion fanout, push fallback, audit log</td>
</tr>
<tr>
<td><span class="mono">/streams</span></td>
<td>device token</td>
<td>stream_sessions, device_commands, recordings</td>
<td>stream command dispatch, media credentials, stream realtime events, push fallback, optional SFU calls</td>
</tr>
<tr>
<td><span class="mono">/recordings</span></td>
<td>device token</td>
<td>recordings</td>
<td>storage object validation, presigned download URL, audit log</td>
</tr>
<tr>
<td><span class="mono">/videos</span></td>
<td>session</td>
<td>videos, devices, MinIO</td>
<td>presigned PUT/GET generation, object listing/deletion</td>
</tr>
<tr>
<td><span class="mono">/push-notifications</span></td>
<td>device token</td>
<td>push_notifications</td>
<td>manual worker dispatch trigger</td>
</tr>
<tr>
<td><span class="mono">/audit</span></td>
<td>device token</td>
<td>audit_logs</td>
<td>none (read only)</td>
</tr>
<tr>
<td><span class="mono">/ops</span></td>
<td>none</td>
<td>DB, MinIO, in-memory metrics, SFU service</td>
<td>readiness checks, metric export</td>
</tr>
<tr>
<td><span class="mono">/admin</span></td>
<td>HTTP Basic auth</td>
<td>MinIO</td>
<td>embedded admin UI + object operations</td>
</tr>
</tbody>
</table>
<h3>Detailed endpoint groups</h3>
<details>
<summary>Devices and Links</summary>
<ul>
<li><span class="mono">POST /devices/register</span>: creates device, sets initial online status, auto-creates links with existing opposite-role devices, returns device token.</li>
<li><span class="mono">GET /devices</span>: lists user devices with computed effective presence status using <span class="mono">DEVICE_ONLINE_STALE_SECONDS</span>.</li>
<li><span class="mono">PATCH /devices/:id</span>: updates mutable metadata and role.</li>
<li><span class="mono">POST /devices/:id/heartbeat</span>: token-authenticated presence update for exact device token/device match.</li>
<li><span class="mono">/device-links</span>: ensures one active camera-client pair and ownership checks.</li>
</ul>
</details>
<details>
<summary>Commands, Events, Streams</summary>
<ul>
<li><span class="mono">POST /commands</span>: only client -> camera, only for active links.</li>
<li><span class="mono">POST /events/motion/start</span>: camera-only; sends realtime to linked clients, queues push if offline.</li>
<li><span class="mono">POST /streams/request</span>: creates stream session + start_stream command + realtime notification.</li>
<li><span class="mono">POST /streams/:id/accept</span>: camera transitions stream to streaming; creates media session and optional SFU bootstrap.</li>
<li><span class="mono">GET /streams/:id/publish-credentials</span>: camera-only credential issuance.</li>
<li><span class="mono">GET /streams/:id/subscribe-credentials</span>: participant credential issuance.</li>
<li><span class="mono">POST /streams/:id/end</span>: closes session, ends SFU (if enabled), creates recording placeholder, notifies both parties.</li>
</ul>
</details>
<details>
<summary>Storage and Recordings</summary>
<ul>
<li><span class="mono">POST /videos/upload-url</span>: session route to mint presigned PUT + metadata row.</li>
<li><span class="mono">POST /recordings/:id/finalize</span>: camera marks recording ready once object exists, or creates simulator placeholder if object key starts with <span class="mono">sim/</span>.</li>
<li><span class="mono">GET /recordings/:id/download-url</span>: requester/camera only, ready-only, verifies object exists before presigning.</li>
</ul>
</details>
</section>
<section id="workers">
<h2>9) Workers and Reliability Mechanisms</h2>
<div class="grid g3">
<div class="card">
<h3>Command retry loop</h3>
<p>Inside realtime gateway. Scans <span class="mono">device_commands</span> where status is <span class="mono">sent</span> and stale by >10s. Re-dispatches every 5s. Fails after 3 retries.</p>
</div>
<div class="card">
<h3>Push worker</h3>
<p>Interval (default 10s) dispatches queued notifications with <span class="mono">nextAttemptAt &lt;= now</span>. Missing push token triggers retry backoff; max attempts configurable.</p>
</div>
<div class="card">
<h3>Recording worker</h3>
<p>Interval (default 30s) marks stale <span class="mono">awaiting_upload</span> recordings as <span class="mono">failed</span> after timeout window (default 30 min).</p>
</div>
</div>
<p class="small">Workers perform startup guards using <span class="mono">hasRequiredTables()</span> so they do not run before migrations are applied.</p>
</section>
<section id="security">
<h2>10) Security Controls and Guardrails</h2>
<div class="grid g2">
<div class="card">
<h3>Implemented</h3>
<ul>
<li><strong>Helmet CSP</strong> with explicit script/style/font/connect/media/image directives.</li>
<li><strong>CORS</strong> tied to <span class="mono">BETTER_AUTH_TRUSTED_ORIGINS</span> (or permissive fallback).</li>
<li><strong>Rate limiting</strong> globally and on high-traffic route groups.</li>
<li><strong>Ownership checks</strong> on almost all queries (user-scoped data access).</li>
<li><strong>Role constraints</strong> (for example client->camera command direction).</li>
<li><strong>Token integrity</strong> via timing-safe HMAC verification.</li>
</ul>
</div>
<div class="card">
<h3>Important caveats</h3>
<ul>
<li>Rate limits are in-memory; not shared across replicas.</li>
<li>Metrics are in-memory counters only (no persistence/export protocol).</li>
<li>Mock push provider treats presence of push token as delivery success.</li>
<li>Mock media provider + SFU scaffold are not production media infrastructure.</li>
</ul>
</div>
</div>
</section>
<section id="config">
<h2>11) Configuration Map (Key Env Variables)</h2>
<table>
<thead>
<tr>
<th>Domain</th>
<th>Variables</th>
<th>Architectural effect</th>
</tr>
</thead>
<tbody>
<tr>
<td>Core server</td>
<td><span class="mono">PORT</span>, <span class="mono">DATABASE_URL</span></td>
<td>listener + DB connectivity</td>
</tr>
<tr>
<td>Auth</td>
<td><span class="mono">BETTER_AUTH_SECRET</span>, <span class="mono">BETTER_AUTH_BASE_URL</span>, <span class="mono">BETTER_AUTH_TRUSTED_ORIGINS</span></td>
<td>session signing, base URL, trusted origins, device token signing</td>
</tr>
<tr>
<td>Presence</td>
<td><span class="mono">DEVICE_ONLINE_STALE_SECONDS</span></td>
<td>effective online/offline projection in device listings</td>
</tr>
<tr>
<td>Storage</td>
<td><span class="mono">MINIO_ENDPOINT</span>, <span class="mono">MINIO_PORT</span>, <span class="mono">MINIO_USE_SSL</span>, <span class="mono">MINIO_ACCESS_KEY</span>, <span class="mono">MINIO_SECRET_KEY</span>, <span class="mono">MINIO_BUCKET</span>, <span class="mono">MINIO_PRESIGNED_EXPIRY_SECONDS</span></td>
<td>object I/O, presign TTL, startup bucket bootstrap</td>
</tr>
<tr>
<td>Media</td>
<td><span class="mono">MEDIA_MODE</span>, <span class="mono">MEDIA_PROVIDER</span>, <span class="mono">TURN_URLS</span>, <span class="mono">TURN_USERNAME</span>, <span class="mono">TURN_CREDENTIAL</span></td>
<td>control plane mode and transport descriptor generation</td>
</tr>
<tr>
<td>Workers</td>
<td><span class="mono">PUSH_WORKER_INTERVAL_MS</span>, <span class="mono">PUSH_MAX_ATTEMPTS</span>, <span class="mono">RECORDING_WORKER_INTERVAL_MS</span>, <span class="mono">RECORDING_STALE_SECONDS</span></td>
<td>queue throughput, retry/failure timing</td>
</tr>
<tr>
<td>Admin</td>
<td><span class="mono">ADMIN_USERNAME</span>, <span class="mono">ADMIN_PASSWORD</span></td>
<td>required to mount admin dashboard route logic</td>
</tr>
</tbody>
</table>
</section>
<section id="code-map">
<h2>12) Code Ownership Map (Where to Modify What)</h2>
<div class="grid g2">
<div class="card">
<h3>Server composition</h3>
<p><span class="mono">index.ts</span>: middleware stack, route mounting, startup ordering, workers, realtime setup.</p>
<h3>Identity</h3>
<p><span class="mono">auth.ts</span>, <span class="mono">middleware/auth.ts</span>, <span class="mono">middleware/device-auth.ts</span>, <span class="mono">utils/device-token.ts</span>.</p>
<h3>Persistence schema</h3>
<p><span class="mono">db/schema.ts</span> + <span class="mono">drizzle/*</span> migrations.</p>
</div>
<div class="card">
<h3>Realtime + command delivery</h3>
<p><span class="mono">realtime/gateway.ts</span> and <span class="mono">routes/commands.ts</span>.</p>
<h3>Streaming control</h3>
<p><span class="mono">routes/streams.ts</span>, <span class="mono">media/*</span>, <span class="mono">routes/recordings.ts</span>.</p>
<h3>Operational views</h3>
<p><span class="mono">routes/ops.ts</span>, <span class="mono">observability/metrics.ts</span>, <span class="mono">routes/admin.ts</span>.</p>
</div>
</div>
</section>
<section id="constraints">
<h2>13) Current Constraints and Scaling Boundaries</h2>
<div class="grid g3">
<div class="card">
<h3>State locality</h3>
<p>Presence, rate-limit counters, metrics counters, and SFU registry are process-local. Horizontal scaling requires external shared state.</p>
</div>
<div class="card">
<h3>Media realism</h3>
<p>Media provider is mock; SFU service is scaffold/noop. Production deployment needs real media infrastructure for reliability and scale.</p>
</div>
<div class="card">
<h3>Queue semantics</h3>
<p>Push and command retries are interval-based polling workers. Throughput, ordering guarantees, and dead-letter handling are minimal.</p>
</div>
</div>
<p class="small">For load-bearing evolution, the natural next architecture step is extracting shared state (Redis/queue), production media plane, and distributed rate/metrics telemetry.</p>
</section>
</main>
</div>
</div>
</body>
</html>