Appearance
Code Patterns & Conventions
This document describes the coding patterns used throughout SafeCall. Follow these conventions when making changes.
Import Patterns
Server importing from lib
typescript
import { db_start } from "../../lib/src/db";
import { mqtt_start } from "../../lib/src/mqtt";
import type { QueryResultBeacon } from "../../lib/src/types/query";Always use relative paths from server/src/ to lib/src/.
Server internal imports
typescript
import { cache_get } from "./cache";
import { tracking_process_mqtt_message } from "./tracking";Web internal imports
typescript
import { assert_response, BeaconsListResponseSchema } from "./lib/contracts";
import { util_fetch, util_notification_show } from "./lib/util";The web frontend has its own lib/ directory — it does not import from the top-level lib/.
Naming Conventions
Functions
- Snake case with module prefix:
cache_get(),mqtt_start(),tracking_prediction_start() - Getters use
_getsuffix:server_db_get(),mqtt_status_get() - Booleans use
_is_or return boolean:tracking_prediction_is_running()
Types
- PascalCase with
QueryResultprefix for DB types:QueryResultBeacon,QueryResultUser - Valibot schemas use
Schemasuffix:MqttButtonAdv1Schema,QueryResultBeaconSchema
Files
- Snake case for all TypeScript files:
ble_scanner.dart,navigation_graph.dart - Type definitions use
.d.tsextension - Test files use
.test.tssuffix
Validation Pattern
All external data is validated with Valibot at runtime:
typescript
import * as v from "valibot";
// 1. Define schema
const MySchema = v.object({
id: v.number(),
name: v.string(),
});
// 2. Parse (throws on invalid data)
const data = v.parse(MySchema, rawData);
// 3. Use assertion helpers in web frontend
import { assert_response } from "./lib/contracts";
const data = assert_response(MySchema, response);HTTP Route Pattern
Routes in server/src/routes/http.ts follow a consistent pattern:
typescript
if (url === "/beacons" && method === "GET") {
const g = guard_auth(req);
if (g instanceof Response) return g;
const { db } = g;
const rows = db.query("SELECT * FROM beacons").all();
const validated = rows.map((r) => v.parse(QueryResultBeaconSchema, r));
return rs_res_ok({ beacons: validated });
}Pattern:
- Match URL and method
- Run auth guard
- If guard returns Response, return it (error)
- Destructure DB/keys from guard result
- Query database
- Validate results with Valibot
- Return JSON response via
rs_res_ok()
WebSocket Pattern
typescript
// Server subscribes client to a topic
server.subscribe(ws, `track-${mac}`);
// Server publishes to subscribers
server.publish(`track-${mac}`, JSON.stringify(update));Guard Pattern
Guards are the first thing called in authenticated route handlers:
typescript
// Admin-only route
const g = guard_admin(req);
if (g instanceof Response) return g;
const { db, jose_keys } = g;
// Any authenticated user
const g = guard_auth(req);
if (g instanceof Response) return g;
const { db, jose_keys, user_id, is_admin } = g;
// DB-only (no auth)
const g = guard_db(req);
if (g instanceof Response) return g;
const { db } = g;Web Page Pattern
Each page is a function that renders HTML and sets up event handlers:
typescript
export async function beacons_page() {
// 1. Check auth
if (!util_user_logged_in()) return;
// 2. Build HTML from templates
const html = template_beacon_listing + template_new_beacon + ...;
document.getElementById("content")!.innerHTML = html;
// 3. Fetch data
const res = await util_fetch("/beacons");
const data = assert_response(BeaconsListResponseSchema, res);
// 4. Render data into DOM
// 5. Attach event listeners
}Flutter Patterns
Screen structure
Each screen is a StatefulWidget with lifecycle in initState and cleanup in dispose.
API calls
dart
final response = await apiClient.fetchBundle(locationId);BLE scanning
dart
scanner.startScan();
final beacons = scanner.getDetectedBeaconsSorted();
final rssi = scanner.getAverageRssi(beaconUname);Config Access
Server modules access configuration through the singleton exported by server.ts:
typescript
import { main_config, server_db_get, server_keys_get } from "./server";Never read main.config.json directly from modules — always go through server.ts exports.