| `ACCESS_PASSWORD` | yes | The shared secret typed into the login form. Compared in constant time. |
| `AUTH_SECRET` | yes | Signing key for the session JWT. Must differ from `ACCESS_PASSWORD` - keep both secret; a leaked signing key lets an attacker forge sessions. |
| `DATABASE_PATH` | no | Path to the SQLite file. Default `./data/app.db`. |
| `LOG_LEVEL` | no | `debug` \| `info` \| `warn` \| `error`. Defaults to `debug` in dev, `info` in production. |
Compose-only (read by `docker-compose.yml` for `${...}` substitution, not by the app itself):
`TRAEFIK_HOST`, `NETWORK_NAME`.
## Database
Schema lives in `lib/db/schema.ts`, migrations in `drizzle/` (generate new ones with `pnpm db:generate`
after a schema change, and commit the generated SQL). A recording is a thin pointer over an already-captured
session's events, not a separate capture pipeline - every live command is always persisted for stats,
regardless of whether the session is later saved as a named recording.
## Docker deployment
```bash
mkdir -p data && chown 10001:10001 data # match the container's non-root uid/gid
cp .env.example .env # fill in real secrets + TRAEFIK_HOST/NETWORK_NAME
docker compose up -d --build
```
**HTTPS in front of this container is not optional.** Web Bluetooth refuses to run outside a secure
context, so if the app isn't served over HTTPS (via Traefik, as the included labels assume, or another
reverse proxy terminating TLS), the Control and Replay pages simply won't be able to use Bluetooth at all.
The SQLite file lives at `./data/app.db` on the host (bind-mounted, read-write) - everything else in the
container filesystem is read-only. Back up by copying that one file; there's no other state.
## CI/CD
`.gitea/workflows/ci.yml` runs lint + build on every push/PR, and on a pushed tag also builds and pushes the
Docker image to this repo's path on the `dev.pivoine.art` Gitea registry, authenticating with a
`PACKAGE_TOKEN` repo secret.
## Usage guide
1. Log in with the shared secret.
2. On **Control**, scan for devices and connect. Move the sliders to control actuators live.
3. Click **Start session** before you begin if you want this session tracked in stats or saved as a
recording; **End session** when done, and optionally name it to save it as a recording.
4. On **Recordings**, click replay on a saved recording, connect the devices you want to use, match them to
the recording's original device slots, and play back.
5.**Sessions** and **Stats** show history and aggregated usage.
## Security & privacy notes
- This is a single shared-secret gate, not multi-user authentication - anyone with the secret has full
access. Treat it like a shared house key; restricting network access (VPN, IP allowlist, Traefik
middleware) in addition to the app secret is recommended for anything beyond trusted personal use.
- All data (recordings, session history, device names) stays in your own SQLite file. Nothing is sent
anywhere except directly between your browser and the devices it connects to, and between your browser
and this server.
- There's no login rate-limiting in this version - acceptable behind a private deployment, but worth
knowing.
## Known limitations
- **iOS/Safari cannot use Web Bluetooth at all** (see above) - not fixable from this app.
- BLE device identity isn't stable across browser sessions; the device-remap step mitigates but can't fully
solve matching two identically-named devices.
- Concurrent BLE connection limits are set by the OS/Bluetooth adapter, not this app.
- Replay timing uses `performance.now()`-relative scheduling to avoid wall-clock drift, but individual
command dispatch still has a few-to-tens-of-milliseconds of jitter.