diff --git a/Backend/docs/release-checklist.md b/Backend/docs/release-checklist.md
new file mode 100644
index 0000000..e53f070
--- /dev/null
+++ b/Backend/docs/release-checklist.md
@@ -0,0 +1,30 @@
+# Release Checklist (Phase 10)
+
+## Functional verification
+- [ ] Device registration and heartbeat flow works for both camera and client roles.
+- [ ] Camera-client linking and command dispatch/ack flows work end-to-end.
+- [ ] Motion start/end events are persisted and notify linked clients.
+- [ ] On-demand stream request/accept/end flow works with media credentials.
+- [ ] Recording placeholder creation, finalize, and download URL generation work.
+- [ ] Push fallback queue delivers notifications when realtime channel is offline.
+
+## Security and reliability
+- [ ] Rate limit thresholds validated in staging.
+- [ ] Audit logs capture stream/event/recording sensitive operations.
+- [ ] Env vars present: auth base URL, media provider, database, MinIO, secrets.
+- [ ] Worker loops running: recordings worker and push worker.
+
+## Observability
+- [ ] `/ops/live` and `/ops/ready` healthy.
+- [ ] `/ops/metrics` reports request counters and status counters.
+- [ ] Structured request logs are visible in deployment log sink.
+
+## Pre-release load and tests
+- [ ] Unit tests pass (`bun test`).
+- [ ] Smoke load script passes (`bun run load:smoke`).
+- [ ] Manual run with simulator in two tabs confirms camera+client happy path.
+
+## Rollout
+- [ ] Deploy to staging and run checklist above.
+- [ ] Rollout production gradually and watch error/status metrics.
+- [ ] Keep rollback procedure ready (previous image/version + migration compatibility).
diff --git a/Backend/package.json b/Backend/package.json
index 541ec08..1928bb5 100644
--- a/Backend/package.json
+++ b/Backend/package.json
@@ -39,6 +39,8 @@
"scripts": {
"start": "bun run index.ts",
"dev": "bun --watch index.ts",
+ "test": "bun test",
+ "load:smoke": "bun run scripts/load-smoke.ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
diff --git a/Backend/public/mobile-sim.html b/Backend/public/mobile-sim.html
index ee43891..7f47e86 100644
--- a/Backend/public/mobile-sim.html
+++ b/Backend/public/mobile-sim.html
@@ -118,9 +118,19 @@
Mobile Client Simulator
- Use this page like a phone app: register device role, connect socket with bearer device token, and run camera/client
- actions. You must already be signed in via Better Auth in this browser session.
+ Use this page like a phone app: authenticate, register device role, connect socket with bearer device token, and run
+ camera/client actions.
+
+ App-Like Onboarding Flow
+
+ - Sign up/sign in in the Auth panel.
+ - Pick role (
camera or client) and register device.
+ - Connect socket to become online.
+ - Client links to camera, requests stream; camera accepts automatically via command handling.
+ - Camera finalizes recording; client fetches download URL and polls push inbox when offline.
+
+
diff --git a/Backend/scripts/load-smoke.ts b/Backend/scripts/load-smoke.ts
new file mode 100644
index 0000000..14eb0e8
--- /dev/null
+++ b/Backend/scripts/load-smoke.ts
@@ -0,0 +1,26 @@
+const baseUrl = process.env.LOAD_BASE_URL ?? 'http://localhost:3000';
+const rounds = Number(process.env.LOAD_ROUNDS ?? 25);
+
+const run = async () => {
+ const start = Date.now();
+
+ for (let i = 0; i < rounds; i += 1) {
+ const [live, ready, metrics] = await Promise.all([
+ fetch(`${baseUrl}/ops/live`),
+ fetch(`${baseUrl}/ops/ready`),
+ fetch(`${baseUrl}/ops/metrics`),
+ ]);
+
+ if (!live.ok || !ready.ok || !metrics.ok) {
+ throw new Error(`Smoke load failed at round ${i + 1}`);
+ }
+ }
+
+ const totalMs = Date.now() - start;
+ console.log(`Completed ${rounds} rounds in ${totalMs}ms`);
+};
+
+run().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/Backend/tests/device-token.test.ts b/Backend/tests/device-token.test.ts
new file mode 100644
index 0000000..141aa69
--- /dev/null
+++ b/Backend/tests/device-token.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, test } from 'bun:test';
+
+import { createDeviceToken, verifyDeviceToken } from '../utils/device-token';
+
+describe('device token', () => {
+ test('roundtrips valid payload', () => {
+ const token = createDeviceToken({
+ userId: 'user-1',
+ deviceId: 'device-1',
+ role: 'client',
+ }, 60);
+
+ const payload = verifyDeviceToken(token);
+
+ expect(payload).not.toBeNull();
+ expect(payload?.userId).toBe('user-1');
+ expect(payload?.deviceId).toBe('device-1');
+ expect(payload?.role).toBe('client');
+ });
+
+ test('rejects malformed tokens', () => {
+ expect(verifyDeviceToken('bad-token')).toBeNull();
+ });
+});
diff --git a/Backend/tests/env.test.ts b/Backend/tests/env.test.ts
new file mode 100644
index 0000000..a9f6fd7
--- /dev/null
+++ b/Backend/tests/env.test.ts
@@ -0,0 +1,25 @@
+import { afterEach, describe, expect, test } from 'bun:test';
+
+import { getBetterAuthBaseUrl, getFirstDefinedEnv } from '../utils/env';
+
+const ORIGINAL_ENV = { ...process.env };
+
+afterEach(() => {
+ process.env = { ...ORIGINAL_ENV };
+});
+
+describe('env helpers', () => {
+ test('getFirstDefinedEnv returns first set value', () => {
+ process.env.TEST_A = '';
+ process.env.TEST_B = ' value-b ';
+
+ expect(getFirstDefinedEnv('TEST_A', 'TEST_B')).toBe('value-b');
+ });
+
+ test('getBetterAuthBaseUrl prefers BETTER_AUTH_BASE_URL over legacy var', () => {
+ process.env.BETTER_AUTH_BASE_URL = 'http://base-url:4000';
+ process.env.BETTER_AUTH_URL = 'http://legacy:3000';
+
+ expect(getBetterAuthBaseUrl()).toBe('http://base-url:4000');
+ });
+});