Skip to content

JDRV-space/siduri

Repository files navigation

Siduri

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.

What It Does

  • 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.

Stack

  • 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

Hard Limits

  • 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.

Install

npm install
cp .env.example .env

Set 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-id

For local development, authenticate the Google client library:

gcloud auth application-default login

Then run it:

npm run dev

Open http://localhost:8080. Register the first account. That account becomes the owner.

GCS Setup

Create a bucket and allow browser uploads:

gsutil mb -l us-central1 gs://YOUR_BUCKET_NAME

Create 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_NAME

The 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_NAME

If that is unacceptable, this app needs code changes for signed read URLs or proxying media. It does not do that now.

Configuration

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.

Deploy

Cloud Run

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

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 \
  siduri

Optional Functions

These are separate Cloud Functions triggered by GCS object finalization. The main app works without them.

  • functions/gif-generator: creates a short .gif beside uploaded MP4/WebM files.
  • functions/video-subtitles: creates .vtt subtitles 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.

API

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

Security Notes

  • 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_ORIGINS in 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.

License

Apache License 2.0. See LICENSE.

Attribution notices are listed in NOTICE.

About

Self-hosted video hosting with viewer analytics. A personal alternative to Vidyard and Loom.

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors