Siduri is a small self-hosted video tool for a single installation. It records or uploads videos in the browser, stores the files in Google Cloud Storage, and keeps metadata, users, share links, and view tracking in SQLite.
The current scope excludes multi-tenant SaaS, private object storage, high traffic, transcoding, and editing.
- Records webcam and microphone video in the browser.
- Supports MediaPipe background blur or static replacement backgrounds.
- Uploads MP4 or WebM files directly to GCS through signed URLs.
- Plays videos with Video.js from public GCS URLs.
- Tracks watch time and completion for tokenized share links.
- Generates share links for a recipient email, with an optional viewer name.
- Sends password reset and view notification emails if SMTP is configured.
- Can run optional Cloud Functions for GIF thumbnails and Spanish subtitles.
- Node.js 18+
- Express
- Vanilla JavaScript frontend, no build step
- SQLite via
better-sqlite3 - Google Cloud Storage for video files
- Optional Google Cloud Functions for subtitles and GIF thumbnails
- Single-tenant by design. The first registered user becomes owner. Extra users need invitations, but this is still one shared installation.
- SQLite is embedded. Run at most one app instance writing to the database.
- Cloud Run with SQLite needs
--max-instances 1. Multiple instances can corrupt or split state. - GCS FUSE persistence works as a cheap option, not as a real database volume. It has locking and latency caveats.
- Videos, subtitles, and GIFs are assumed to be publicly readable GCS objects. A share URL is not strong access control, and tracking only happens for links with a valid viewer token.
- No transcoding, clipping, trimming, adaptive streaming, or format repair. Upload browser-playable MP4 or WebM.
- App upload limit is 100 MB per video and 10 uploads per user per hour.
- The GIF function has its own guards: 500 MB max input, 10 minutes max duration, 60 second ffmpeg timeout.
- Subtitle generation is optional, Spanish-focused, slow, and can time out on long or noisy videos.
- MediaPipe blur depends on browser support, WebGL, and CDN resources allowed by the CSP.
- This is not for high-traffic public SaaS.
npm install
cp .env.example .envSet at least these values in .env:
JWT_SECRET=use-a-random-string-at-least-32-characters-long
GCS_BUCKET=your-video-bucket
GCS_PROJECT_ID=your-gcp-project-idFor local development, authenticate the Google client library:
gcloud auth application-default loginThen run it:
npm run devOpen http://localhost:8080. Register the first account. That account becomes the owner.
Create a bucket and allow browser uploads:
gsutil mb -l us-central1 gs://YOUR_BUCKET_NAMECreate cors.json:
[
{
"origin": ["*"],
"method": ["GET", "PUT", "POST", "DELETE", "OPTIONS"],
"responseHeader": ["Content-Type", "Authorization"],
"maxAgeSeconds": 3600
}
]Apply it:
gsutil cors set cors.json gs://YOUR_BUCKET_NAMEThe current player uses https://storage.googleapis.com/... URLs. Make objects publicly readable if you want playback to work as written:
gsutil iam ch allUsers:objectViewer gs://YOUR_BUCKET_NAMEIf that is unacceptable, this app needs code changes for signed read URLs or proxying media. It does not do that now.
| Variable | Required | Default | Notes |
|---|---|---|---|
JWT_SECRET |
Yes | none | Use a random 32+ character value. |
GCS_BUCKET |
Yes | none | Bucket used for videos/ objects. |
GCS_PROJECT_ID |
Yes | none | Google Cloud project ID. |
PORT |
No | 8080 |
Cloud Run expects 8080. |
NODE_ENV |
No | development |
Use production behind HTTPS. |
DATA_DIR |
No | ./data |
Directory for siduri.db. |
BASE_URL |
No | auto-detected | Public URL used in share links. |
ALLOWED_EMAIL_DOMAINS |
No | all domains | Comma-separated registration allowlist. |
ALLOWED_ORIGINS |
No | localhost origins | Comma-separated CORS allowlist. |
SMTP_HOST |
No | none | Enables password reset and email notifications with the other SMTP values. |
SMTP_PORT |
No | 587 |
SMTP port. |
SMTP_USER |
No | none | SMTP username. |
SMTP_PASS |
No | none | SMTP password. |
SMTP_FROM |
No | SMTP_USER |
Sender address. |
Basic deployment:
gcloud run deploy siduri \
--source . \
--region us-central1 \
--allow-unauthenticated \
--max-instances 1 \
--memory 512Mi \
--set-env-vars "JWT_SECRET=YOUR_SECRET,GCS_BUCKET=YOUR_BUCKET,GCS_PROJECT_ID=YOUR_PROJECT,NODE_ENV=production"By default, SQLite lives in the container filesystem. Treat that as disposable. Redeploys, restarts, and instance replacement can lose the database.
If you accept the caveats, mount a separate GCS bucket for the SQLite data directory:
gsutil mb -l us-central1 gs://YOUR_BUCKET_NAME-data
gcloud run services update siduri \
--region us-central1 \
--add-volume name=data-vol,type=cloud-storage,bucket=YOUR_BUCKET_NAME-data \
--add-volume-mount volume=data-vol,mount-path=/app/data \
--max-instances 1 \
--set-env-vars "DATA_DIR=/app/data"Do not remove --max-instances 1 while using SQLite.
docker build -t siduri .
docker run -p 8080:8080 \
-e JWT_SECRET=your-secret-key-32-chars-minimum \
-e GCS_BUCKET=your-bucket \
-e GCS_PROJECT_ID=your-project \
-e NODE_ENV=production \
-v $(pwd)/data:/app/data \
siduriThese are separate Cloud Functions triggered by GCS object finalization. The main app works without them.
functions/gif-generator: creates a short.gifbeside uploaded MP4/WebM files.functions/video-subtitles: creates.vttsubtitles with faster-whisper, configured for Spanish.
Deploy from each function directory with its deploy.sh. Set GCS_BUCKET first. The subtitle function can also use HF_TOKEN.
The app mounts the API at /api. It also mounts /video/studio/api for the existing load-balancer path setup.
| Area | Endpoints |
|---|---|
| Auth | POST /api/auth/register, POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me, POST /api/auth/refresh |
| Password reset | POST /api/auth/forgot-password, POST /api/auth/reset-password |
| Invitations | POST /api/auth/invitations, GET /api/auth/invitations |
| API tokens | POST /api/auth/api-token, GET /api/auth/api-tokens, DELETE /api/auth/api-tokens/:id |
| Upload | POST /api/upload |
| Videos | POST /api/videos, GET /api/videos, GET /api/videos/:id, PATCH /api/videos/:id, DELETE /api/videos/:id |
| Sharing | POST /api/videos/:id/share |
| Tracking | POST /api/track, POST /api/track/beacon |
| Notifications | GET /api/settings/notifications, POST /api/settings/notifications/teams, POST /api/settings/notifications/email |
| Health | GET /health |
- JWTs are stored in httpOnly cookies. Production mode sets secure cookie behavior for HTTPS.
- Passwords are hashed with bcrypt.
- Login and registration are rate limited to 10 attempts per 15 minutes.
- General API requests are rate limited to 60 requests per minute.
- Helmet is enabled with a CSP that allows the CDN and WebAssembly needs of Video.js and MediaPipe.
- CORS defaults to localhost. Set
ALLOWED_ORIGINSin production. - GCS object privacy is the weak point. Public object URLs are easy to forward.
- Signed upload URLs expire after one hour, but anyone with a leaked valid URL can upload during that window.
Apache License 2.0. See LICENSE.
Attribution notices are listed in NOTICE.