Перейти до основного вмісту

🖥️ Backend Overview

Fastify monolith with 18 modules under src/modules/, each self-contained. Most follow Routes / Repository / Service. Two use DDD (device-core/, automations/), one bridges to Python (device-bootstrap/).

Tech Stack

  • Runtime: Node.js (ESM)
  • Framework: Fastify + Zod + @fastify/swagger + Scalar UI
  • DB: PostgreSQL 16 via pg (no ORM, raw SQL with query<T>())
  • MQTT: mqtt library, broker is Mosquitto in same Docker stack
  • WebSocket: @fastify/websocket for local frontend, separate WSS client to Cloud
  • BLE: Python bleak via subprocess (scripts/ble_bridge.py)

Module Map

ModuleLayoutDB TablesEndpoints
authStandardusers, refresh_tokens/api/auth/*
usersStandardusers (cloud-synced read)/api/users/*
user-managementStandardusers, station_members/api/user-management/*
devicesStandarddevices/api/devices/*
device-typesStandarddevice_types (registry sync)/api/device-types
device-eventsStandarddevice_events, event_settings, device_type_event_presets/api/device-events/*
device-bootstrapService + Python bridge— (provisioning candidates in memory)/api/device/*
device-coreDDDdevices, device_types— (event-driven, no REST)
zonesStandardzones/api/zones/*
automationsDDDautomations, automation_runs/api/automations/*
kitsStandarddevice_kits, kit_presets/api/kits/*
firmwareStandardfirmwares/api/firmware/*
otaStandardota_logs/api/ota/*
wifiStandard— (system calls)/api/wifi/*
settingsStandardstation_settings/api/settings/*
backupService-only(reads/writes all)/api/backup/*
cloudConfig + WS clientcloud_config/api/cloud/status
cloud-syncSync serviceusers (write)— (WSS-driven)
stationRepository-onlystation, station_members

DB Schema

Migrations in src/db/migrations/ — sequential 001_, 002_, ... applied on boot via runMigrations().

Station DB Schema

Conventions

Routes

export const myRoutes = (fastify: FastifyInstance) => {
fastify.addHook('preHandler', verifyToken);

fastify.post(
'/',
{ preHandler: authorize(['owner', 'admin']) },
async (request, reply) => {
const body = CreateSchema.parse(request.body);
const result = await myService.create(body);
return reply.status(201).send(result);
},
);
};
  • Routes registered in app.ts with prefix: app.register(myRoutes, { prefix: '/api/my' })
  • All Zod schemas use camelCase keys
  • Errors via reply.status(code).send({ message }) — global setErrorHandler shapes the response

Repositories

import { query } from '../../db/db.js';
import type { Device } from '@smart-home/shared';

export const findById = async (id: string): Promise<Device | null> => {
const result = await query<Device>('SELECT * FROM devices WHERE id = $1', [id]);
return result.rows[0] ?? null;
};
  • query<T>() auto-converts snake_case → camelCase in result rows
  • Parameterized SQL only ($1, $2, ...)
  • Returns null for not found, no exceptions for normal absence

Services

  • Import repository as import * as myRepo from './myRepository.js'
  • No direct query() — all SQL through the repository
  • Use logger (never console, never fastify.log)

Auth Hooks

HookVerifiesSource
verifyTokenJWT Bearer (user session)hooks/authHooks.ts
authorize(roles)Global role: owner/admin/memberhooks/authHooks.ts
requireStationRole(...)Station-membership-scoped rolehooks/authHooks.ts
verifyDeviceTokenDevice-to-backend auth (per-device token)hooks/deviceAuthHooks.ts
verifyAgentTokenstation-agent talking to backendhooks/deviceAuthHooks.ts

Boot Sequence

Source: src/server.ts

Periodic Maintenance

JobIntervalPurpose
Heartbeat timeoutevery minuteMark device offline if no heartbeat for 3 min
Refresh token cleanup24hDelete expired refresh_tokens rows
Automation runs cleanup24hDelete automation_runs older than 30 days
Device events cleanup24hDelete device_events older than 30 days

Reference