Backend¶
The backend is a Cargo workspace with two crates:
| Crate | Binary | Purpose |
|---|---|---|
crates/vistilla-api |
vistilla-api |
The server: pages, API, media, workers. |
crates/vistilla-cli |
vistilla-cli |
A small command-line client that signs in and uploads files. See Command-line tools. |
Both use Rust edition 2021. The release profile uses opt-level = 2,
codegen-units = 16 and no LTO, to keep peak build memory low on small
build machines.
Source layout (crates/vistilla-api/src)¶
| Module | Responsibility |
|---|---|
main.rs |
Start-up, the passwd subcommand, AppState, graceful shutdown. |
config.rs |
Reads every VISTILLA_* environment variable into Config. |
db.rs |
Opens SQLite, applies the embedded migrations. |
auth.rs |
Password hashing, sessions, the Auth / OptionalAuth extractors, user creation. |
perms.rs |
The Role type (viewer < member < admin < owner) and its checks. The access rules themselves (team_access, video_access, video_for_edit) are in routes/mod.rs. |
error.rs |
AppError, mapping errors to HTTP status and {"error": …}. |
ids.rs |
Random ids and tokens, SHA-256 token hashing, slugs, now_ms(). |
crypto.rs |
Sealer: XChaCha20-Poly1305 encryption of stored AI keys. |
mail.rs |
The designed HTML (and plain-text) email template. |
delivery.rs |
Sending email, at once: SMTP, Brevo or the mail relay. |
jobs.rs |
The background job worker (seek index, silences, transcripts, summaries, sweeps). |
seekable.rs |
Rewrites recordings so they can be seeked, and applies trims (ffmpeg). |
webm_index.rs |
Adds the index to a WebM too big for the disk, as it streams back to the store in parts. |
silences.rs |
Finds long pauses for "skip silences" (ffmpeg). |
web.rs |
Page routes, link-preview tags, /sw.js, /favicon.svg. |
storage/ |
fs.rs (filesystem), s3.rs + sigv4.rs (S3-compatible). |
ai/ |
Providers, audio handling, transcription, summaries. See AI pipeline. |
routes/ |
One module per API area: auth_routes, recovery, teams, collections, videos, comments, shares, moments, intelligence, ai_settings, analytics, notifications. |
Start-up sequence (main.rs)¶
- Set up logging with
tracing-subscriber. The filter comes fromVISTILLA_LOG(defaultinfo,sqlx=warn). - Read the configuration from the environment.
- Connect to SQLite and apply any migrations not applied yet.
- Open the object store. For S3, make sure the bucket exists; a failure is only a warning.
- Load the encryption key (
VISTILLA_SECRET_KEY, or asecret.keyfile it creates). - If
VISTILLA_RUN_WORKERSis on (the default):- put any job left
runningby a crash back topending; - spawn the jobs worker.
- put any job left
- Build the router, wrap it in
TraceLayer, bind toVISTILLA_BIND(default0.0.0.0:8787) and serve. - Shut down gracefully on Ctrl-C or SIGTERM.
AppState, shared by every handler, holds:
- the
SqlitePool; - the
Storage; - the
Config; - the
Sealer.
Routing (routes/mod.rs)¶
The router is assembled from:
/api/*: the JSON API. See the HTTP API reference./media/*: video, thumbnail, caption and download responses. These sit outside/apiso a<video src>can point straight at them./assets/*:ServeDiroverVISTILLA_WEB_ROOT/assets, withCache-Control: no-cache./docs/*:ServeDiroverVISTILLA_DOCS_ROOT(this site), with/docsredirecting to/docs/.- Pages from
web.rs.
A middleware adds security headers to every response. See Security.
Request size limits¶
| Route | Limit |
|---|---|
PUT /api/videos/{id}/blob (whole-file upload) |
No body limit. Checked against VISTILLA_MAX_UPLOAD_BYTES while streaming. |
PUT /api/videos/{id}/parts/{n} |
64 MiB per part; all parts together up to VISTILLA_MAX_RECORDING_BYTES. |
PUT /api/videos/{id}/thumbnail |
8 MiB. |
PUT /api/me/avatar |
1 MiB at the router; the handler refuses over 512 KiB (413), and anything but JPEG, PNG or WebP. |
| Everything else | axum's default (2 MB). |
Errors¶
Handlers return AppResult<T>. AppError maps to HTTP like this:
| Variant | Status | Body |
|---|---|---|
BadRequest(msg) |
400 | {"error": msg} |
Unauthorized |
401 | {"error": "authentication required"} |
Forbidden(msg) |
403 | {"error": msg} |
NotFound(what) |
404 | {"error": "<what> not found"} |
Conflict(msg) |
409 | {"error": msg} |
TooLarge |
413 | {"error": "payload too large"} |
Database / Internal |
500 | {"error": "internal error"}. The real cause is only logged. |
Background workers¶
Jobs worker (jobs.rs)¶
It loops forever and does one unit of work per tick, trying these in order:
seekable::index_next: make a new recording seekable, and apply its trim.silences::detect_next: find long pauses.run_next_transcriptrun_next_summarysweep_abandoned_upload: discard live uploads with no new part for 7 days.backfill: for workspaces that have AI turned on, newest video first, queue a missing transcript, then a missing summary, then a summary that has no chapters yet.
When a tick finds nothing to do, the worker sleeps 5 seconds. Jobs are
claimed with a single
UPDATE … SET status='running' … WHERE video_id = (SELECT … LIMIT 1) RETURNING
statement, so two workers can never take the same job. The HTTP client used
for AI calls has a 600-second timeout. Jobs left running when the server
stopped are put back to pending at start-up.
With S3 storage and ffmpeg, a transcript job does not download the video.
ffmpeg reads it from the bucket over a signed link and keeps only its sound
track (a few megabytes an hour), and the rest of the job works on that. The
pause finder (silences.rs) reads from the link the same way. With folder
storage, or without ffmpeg, the video is downloaded to a temporary file.
A transcript job whose video has no length (duration_ms 0) measures it
first: from the file's header with ffprobe, or, for a file straight from
MediaRecorder that has none, by reading it through with ffmpeg
(-c copy -f null). Without it a long recording would be sent in one piece
instead of in windows. Each job logs when it starts.
External tools¶
The runtime image installs ffmpeg (and its ffprobe). It is used to:
- make recordings seekable and trim them;
- detect silences;
- measure loudness;
- extract and re-encode audio for the AI.
Without ffmpeg most of Vistilla still works: recordings are published untrimmed
and unindexed, silences are not found, and audio is sent to Gemini or Whisper
as the original file. Transcription with the chat models that hear audio
(OpenAI gpt-audio-1.5, Qwen Omni) needs ffmpeg, because they only accept
MP3 or WAV.