Backend: System Components¶
Goal and Context¶
The backend provides a REST API for the Rooms system and encapsulates the business logic around users, roles, rooms, tasks, events, displays, and check-ins. The API is implemented in Go and follows a controller model service structure where controllers handle HTTP, models encapsulate data access, and services contain domain specific helper logic such as renderer, QR, and mail. In production, Postgres is typically used while SQLite is available for local development and tests.
Architecture and Request Flow¶
The HTTP layer is strictly separated from data access and business logic. The entry point initializes configuration and infrastructure (DB/Redis/JWT) and starts the router. Requests pass through global middleware, then route-specific middleware (auth/permissions/validation), before a controller is invoked.
cmd/rooms/main.gocreates the main router (Gin Default) and setsCORS_adjust.- All
/v1/*requests are forwarded torouter.NewRouter(). router.NewRouter()registers logger/recovery and theDatabaseHealthCheck.- Route groups define auth/permission/validation and invoke controllers.
- Controllers access models/services (DB/Redis/Python renderer).
- Responses are returned as JSON with clear status codes.
Go (API)¶
The API is based on Gin with a clear routing structure. Database access is handled via GORM and is automatically migrated on startup. JWT handling uses ES256 and reads keys from PEM files, so no symmetric keys need to be stored in plaintext.
- Router: Gin with
/v1as the base path. - ORM: GORM, AutoMigrate for
User,Role,Permission,Room,RoomStatus,Display,Task,TaskType,Event,EventType,QRCode,CheckIn. - JWT (ES256) via
jwt_handlingwith PEM keys. - Config loader:
config.Load()reads.envand validates required fields based on.env.example. - Audit logging via
internal/pkg/logger(structured slog logs).
Postgres (and optional SQLite)¶
Postgres is the production database and is configured via environment variables. On startup, roles and permissions are synchronized so the API has a defined authorization baseline. SQLite is available as a fast, file-based mode and is primarily used in tests.
- DB type via
database_type(postgresorsqlite). - Postgres DSN is built from environment variables.
- Roles and permissions are loaded on startup from
app/backend/configs/roles.yaml(orROLES_CONFIG_PATH) and synchronized into the DB. - A standard admin is created on startup if not present (
standard_admin_*) (isSUPER admin). - A placeholder user
deleted_useris automatically created and used for reassignments.
Redis¶
Redis is used as volatile storage for auth states. Crucial is the token blacklist so logout takes effect immediately even though JWTs are stateless. Additionally, a deactivation flag is used and checked in the auth middleware.
- Required in production (server will not start if
redis_addris missing). - Logout blacklist: tokens are stored for the JWT lifetime.
- Deactivation flag:
user_deactivated_<uuid>prevents logins.
Python Renderer (E-Paper)¶
The renderer generates images for display devices and intentionally runs outside of Go to reuse existing Python logic and templates. Go passes JSON structures and receives a Base64-encoded image, which is delivered directly to the displays.
- Implemented in
app/backend/internal/epaper_rendering. - Go starts the module via
python -m epaper_rendering.render. - Input is JSON for office or event rooms and the output is a Base64 image.
- Format is
pngor1bitwith optionalwidthandheight.
Email (Verification)¶
User verification is sent via SMTP. The link points to a backend endpoint that, after successful verification, redirects to the frontend app or returns JSON depending on the Accept header.
services.SendVerificationEmailuses SMTP configuration from environment variables.- Verification link:
/v1/user/verify?token=<token>.
Danger
Verification emails are not sent to these domains: example.com, example.net and example.org.
Please make sure to use one of these domains or any valid email address under your control for test purposes. Don’t use “fantasy” email addresses (not even for test purposes) that aren’t under your control (e.g., my-funny-test-address@gmail.com, room-test@test-it.com).
Logging and Auditing¶
Logging is based on slog and outputs structured entries. For security-relevant actions, there are audit logs that include the user, the action, and the target object.
- Log level configurable via
LOG_LEVEL. - Audit events via
logger.Audit(...).
Configuration¶
Configuration is controlled via environment variables and loaded on startup by config.Load(). Required values are derived from app/backend/.env.example and if anything is missing startup fails so the API does not run in a partially configured state. If present a .env file is automatically loaded and can be used for local development. Changes to configuration only take effect after a server restart.
Warning
The file app/backend/.env.example must remain in the repository. The config.Load() function reads this file to determine which environment variables are required. Without it, the server cannot start.
Server and Logging¶
These values control where the HTTP server listens and how verbose logging is. Optionally, a separate host can be set for Swagger.
server_host,server_port: bind address and port of the HTTP server.swagger_host: optional host for the Swagger UI (fallback toserver_host:server_port).LOG_LEVEL: log level (DEBUG,INFO,WARN,ERROR).
Admin Bootstrap¶
On first startup, a standard admin is created if it does not exist. These values must be set, otherwise startup fails.
standard_admin_username: username of the initial admin.standard_admin_displayname: display name of the admin.standard_admin_password: initial password (stored hashed). Must meet password requirements: minimum 12 characters, maximum 128 characters.
Database¶
The DB type is selected via database_type. For SQLite a local path is sufficient and for Postgres connection parameters are required.
database_type:sqliteorpostgres.database_path: path for SQLite (e.g.database.db).postgres_host,postgres_port,postgres_database,postgres_username,postgres_password,postgres_sslmode.
Redis¶
Redis is used for logout blacklisting and deactivation flags. If redis_addr is missing, the server will not start.
redis_addr: host:port, e.g.localhost:6379.redis_password: optional password.redis_db: DB index (int).
JWT¶
JWTs are signed with ES256 and keys are provided as PEM files. Validity values control token lifetimes.
jwt_issuer: issuer claim for tokens.jwt_private_key_path: path to the private PEM key.jwt_public_key_path: path to the public PEM key.jwt_valid_duration_in_h: access token validity in hours.refresh_token_valid_duration_in_days: refresh token validity in days.
Roles and Permissions¶
The role definition can be overridden via a YAML file. Roles/permissions are synchronized on startup. The file app/backend/configs/roles.yaml can be used as a template for custom role configurations.
ROLES_CONFIG_PATH: path to the roles config (default:app/backend/configs/roles.yaml).
Frontend and QR Codes¶
These values are used to generate QR code links pointing to the frontend.
frontend_base_url: base URL of the frontend.qr_checkin_path: path for check-in QRs.qr_room_path: path for room QRs.
Display¶
display_refresh_interval_seconds:default = 20, the default refresh interval for the display (used for rendering logic, including QR code generation and clean-up). If you want to change the display refresh interval, you need to configure it in the Display Setup. After modifying the hardware, update this variable so that the backend knows the correct interval and can perform tasks such as calculating QR codes based on it. A shorter interval results in higher power consumption.
QR Code Cleanup¶
qr_code_cleanup_interval_hours: interval for periodic removal of expired, unused QRs.
Email¶
SMTP configuration is used for verification emails.
EMAIL_SERVER,EMAIL_SERVER_PORT,EMAIL_SERVER_USER,EMAIL_SERVER_PASSWORD.
Python Renderer¶
python_executable: optional Python binary for the renderer (default:python3, with venv auto-detection).
Data Model (Details)¶
The models are designed for a relational DB and use UUIDs as primary keys. Many-to-many relationships are represented via join tables, sometimes with additional fields (e.g. RoomUser.Description). Via Base, standard fields like UUID and timestamps are consistently provided.
Interactive Schema Map¶
The diagram below shows the live database structure. You can zoom, pan, and hover over relationships to see the foreign key constraints.
Base Type¶
Base contains UUID and timestamps and is embedded in multiple entities. This ensures consistent fields and reduces boilerplate in the models.
Base(UUID, CreatedAt, UpdatedAt, DeletedAt) forRoom,Display,RoomStatus,Task,Event,QRCode,CheckIn.
Users/Roles/Permissions¶
The authorization model is role based. Roles have a Level that represents a hierarchy where users may only manage roles below their highest level and admins may manage everything. Permissions are centrally defined so controllers and middleware can check against stable names.
Userhas manyRole,Rolehas manyPermission.- Hierarchy via
Role.Level: only roles with a lower level are allowed (except admin). models.DefinedPermissionscentrally defines all permission names.- Role configuration supports wildcards like
resource:*andall.
Permissions List¶
All available permissions are defined in app/backend/internal/models/permission.go:
| Category | Permissions |
|---|---|
| System | system:admin, system:audit |
| User | user:read, user:create, user:update, user:delete, user:manage_roles, user:reset_password |
| Role | role:read, role:create, role:update, role:delete, role:manage_permissions |
| Permission | permission:read |
| Room | room:read, room:create, room:update, room:delete, room:assign_users |
| Room Status | room_status:read, room_status:update |
| Task | task:read, task:create, task:update, task:delete, task:assign |
| Event | event:read, event:create, event:update, event:delete, event:manage_participants |
| Display | display:read, display:create, display:update, display:delete, display:regenerate_key |
| QR Code | qr_code:read, qr_code:create, qr_code:update, qr_code:delete |
| Check-in | checkin:read, checkin:perform, checkin:create, checkin:delete |
Rooms¶
A room is the central entity and can have displays, status, and assigned users. User assignment is intentionally limited because e-paper layouts can only display a small number. Room status is updated via separate endpoints.
RoomwithRoomStatus(1:1),Display(1:n),RoomUser(n:m).RoomUsercontains additional fields such asDescription.- Maximum of 4 users per room (limit in
RoomUser).
Tasks/Events¶
Tasks and events share a common scheduling mechanism so recurrence and time windows are processed consistently. For events, it is additionally ensured that only bookable rooms are used.
TaskandEventshareSchedulableBase.SchedulableBaseencapsulates recurrence logic (recurrence_interval,recurrence_unit,recurrence_end_at).Eventcan only be created inRoom.IsBookable = true.
QRCode/CheckIn¶
QR codes are time-limited tokens that point to rooms or tasks. Check-ins connect a user with a task or a room at a specific time, providing a history for analysis or dashboard display.
QRCodebelongs to aRoom(required), optionally to aTask, and is time-valid.CheckInreferences aRoom, optionally aTask, and optionally aQRCode.- Check-ins are used for history and analytics.
Directory Structure¶
Overview of the most important directories in app/backend:
| Directory | Purpose |
|---|---|
internal/models/ |
Database models and DTOs |
internal/api/v1/controller/ |
HTTP handlers for API endpoints |
internal/config/ |
Environment variable and role configuration loading |
internal/database/ |
DB connections and initialization |
internal/services/ |
Business logic (QR code generation, email, rendering) |
internal/validators/ |
Custom validators for API requests |
configs/ |
roles.yaml with role hierarchy |