Skip to content

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 _get suffix: server_db_get(), mqtt_status_get()
  • Booleans use _is_ or return boolean: tracking_prediction_is_running()

Types

  • PascalCase with QueryResult prefix for DB types: QueryResultBeacon, QueryResultUser
  • Valibot schemas use Schema suffix: MqttButtonAdv1Schema, QueryResultBeaconSchema

Files

  • Snake case for all TypeScript files: ble_scanner.dart, navigation_graph.dart
  • Type definitions use .d.ts extension
  • Test files use .test.ts suffix

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:

  1. Match URL and method
  2. Run auth guard
  3. If guard returns Response, return it (error)
  4. Destructure DB/keys from guard result
  5. Query database
  6. Validate results with Valibot
  7. 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.