silo.guest-pass issues short-lived public links for tightly scoped media access. An authenticated Silo operator can invite a friend to one show or movie without provisioning an account: each pass carries its own token, scope, expiry, and per-pass policy knobs (concurrency cap, watermark, PIN, IP/country allowlist, etc.).
Lives under Sharing. The Sharing category currently contains two plugins:
silo.guest-pass(this) — one-to-one invites to a specific item or library slice.silo.public-catalog— a public-facing landing/advertising page for the library.
| Type | ID | Purpose |
|---|---|---|
http_routes.v1 |
guest-pass |
Admin UI and API at /admin/* and /api/admin/*, plus the public pass page at /p/*, public API at /api/public/*, and static assets at /assets/*. |
scheduled_task.v1 |
maintenance |
Cron 17 */6 * * *. Prunes guest_pass_events rows older than audit_retention_days. |
Standalone sharing layer. The plugin does not depend on other plugins for core operation:
- Catalog browsing in the admin UI goes through the Silo host SDK (
ListLibraryMedia). - Final content-ID → media-file-ID resolution at pass-creation time reads
public.media_filesdirectly (aSELECTgrant on that single table is required until the SDK exposes a resolver call). - Guest playback flows through the host's scoped stream grants — the plugin never exposes broad library permissions to the public route.
Host: Silo-Server/silo-server. SDK: Silo-Server/silo-plugin-sdk.
- Postgres, in a dedicated
guest_passschema. The plugin owns its own tables (guest_passes,guest_pass_grants,guest_pass_events, app config) and runs its own migrations on startup viagolang-migrate.
Each pass is created by an admin and stored with only a sha256 hash of its URL-safe token (32 bytes of entropy) — the plaintext token is shown to the operator exactly once. Optional PINs are hashed with bcrypt; legacy sha256 PIN hashes are upgraded in place on first successful verify.
A pass targets either a specific item or a library slice via target_type / target_id and carries the policy that governs the recipient's access:
- Expiry. Hard
expires_at, plus an optionalvalid_hours_after_first_openwindow so the clock starts when the recipient actually opens the link.StatusandEffectiveExpiresAttake the earlier of the two. - Usage caps.
max_opens,max_plays,max_watch_minutes,max_concurrent_streams,max_devices. Open and play counters are enforced inside theUPDATEstatement so there is no TOCTOU race. Concurrent-stream slots are reserved transactionally viaguest_pass_grantswith aFOR UPDATEon the parent pass row, and can be released if a downstream step (host stream mint) fails. - Playback policy.
max_resolution,allow_downloads,allow_direct_play,disable_seeking,session_grace_minutes,per_item_play_count. - Identity binding.
require_pin+ bcrypt-hashed PIN,lock_to_first_ip(binds to the first observed client IP),ip_allowlist(bare addresses or CIDR),country_allowlist,geofence. - Watermarking.
watermark_mode,watermark_profile,watermark_logo_url, with{{ip}}substitution available in profiles. - Lifecycle.
revoked_atis set by the admin endpoint and short-circuitsStatustorevoked. Status also rolls forward toexpired,open_limit_reached, orplay_limit_reachedas caps are hit.
Per-request client IP feeds
lock_to_first_ip,ip_allowlist, audit rows, and the{{ip}}watermark token. It is read from theX-Silo-Client-IPheader injected by the host — the plugin deliberately does not fall back toX-Forwarded-Forfrom arbitrary callers, since that would let guests spoof their own IP. Until the host stamps that header, IP-derived features are inert and audit rows record an empty IP.
Every guest-pass interaction can be logged to guest_pass_events (pass id, event type, IP, user agent, free-form attrs JSON, timestamp). RecordEvent is best-effort — the public flow does not block on logging. ListEvents exposes the 200 most recent rows per pass to the admin UI.
The maintenance scheduled task runs every six hours and calls PruneEvents(retentionDays), which deletes audit rows older than audit_retention_days (default 180). Retention should be tuned to match your privacy policy.
| Key | Required | Description |
|---|---|---|
database_url |
yes | Postgres DSN for the guest_pass schema. The role only needs ownership of guest_pass plus SELECT on public.media_files. |
public_base_url |
no | Absolute URL used when returning share links. Empty returns plugin-relative paths; set this when Silo sits behind a reverse proxy and links need an absolute external origin. Validated as an absolute URL on configure. |
audit_retention_days |
no | Days of audit history to keep. Defaults to 180; values below 1 fall back to the default. |
Example DSN:
postgres://plugin_guest_pass:password@postgres:5432/silo?search_path=guest_pass&sslmode=disable
Database setup:
CREATE ROLE plugin_guest_pass WITH LOGIN PASSWORD '<chosen>';
CREATE SCHEMA guest_pass AUTHORIZATION plugin_guest_pass;
GRANT CONNECT ON DATABASE silo TO plugin_guest_pass;
GRANT USAGE ON SCHEMA public TO plugin_guest_pass;
GRANT SELECT ON public.media_files TO plugin_guest_pass;Migrations under internal/migrate/files are applied automatically on startup; the operator only needs to create the schema and grant the connect role.
docs/operations.md— day-two runbook: config surfaces, the maintenance task, observability, lifecycle, credential rotation.docs/debugging.md— symptom-first triage for first-open expiry, proxy 404s, cap surprises, VPN/IP lock-outs, country-header gaps, watermarking, and the catalog grant.docs/pass-policy-reference.md— every policy knob (expiry, caps, identity binding, watermarking) with defaults, enforcement site, and cross-field invariants.docs/recipient-experience.md— what the guest sees, status strings, recipient-visible audit footprint, and recovery workflow.docs/data-model.md— table-by-table walkthrough, transaction patterns, thepublic.media_filescross-schema reach, and backup notes.
make build
make testCI builds linux-amd64 binaries on push to main via the reusable workflow in RXWatcher/silo-plugin-repository and publishes them to the catalog at ./binaries/.