A self-hosted video clip sharing service, like a minimal Streamable or YouTube. Upload a clip, it gets transcoded to HLS in the background, and it's playable and shareable via a link for as long as the server stays up.
This is a personal side project with no long-term maintenance commitment — changes are kept minimal and pragmatic rather than over-engineered.
Backend
- .NET 10.0
- Entity Framework Core
- PostgreSQL
- FFmpeg (via Xabe.FFmpeg)
- HLS (HTTP Live Streaming)
Frontend
- Vue 3
- Tailwind v4
Three .NET projects plus one Vue SPA, backed by PostgreSQL:
- ClipViewer.API — ASP.NET Core web API that also hosts the built Vue SPA as static files. Handles auth, video CRUD, and file uploads. Does not transcode video itself.
- ClipViewer.Worker — a separate background-service process that polls the database for pending conversion jobs and does the actual FFmpeg work.
- ClipViewer.Data — shared EF Core
ApplicationDbContextand entity models, referenced by both the API and the Worker. - clipviewer.vue — the Vue 3 + Tailwind frontend, built to
clipviewer.vue/distand served by the API in production (proxied to the Vite dev server on:5173in development).
The API and Worker are decoupled entirely through the Postgres database — there's no in-process queue
or message broker. An upload writes a VideoClip row and a VideoConversionJob row (Pending) to the
DB; the Worker polls for the oldest pending job, transcodes it to HLS and generates a thumbnail, then
updates the clip and marks the job complete. Job progress is written back to the DB as FFmpeg reports
it, which is how the frontend polls for conversion progress.
Clone the repository
git clone https://github.com/DevRuto/ClipViewer.git cd ClipViewerConfigure secrets
cp .env.example .env
Edit
.envand setPOSTGRES_PASSWORDandJWT_SECRETto real values (e.g.openssl rand -base64 48for the JWT secret). The API refuses to start with a missing or placeholderJWT_SECRET.Start the application
docker compose up --build
Access the application
- App: http://localhost:5000
- Database: PostgreSQL on port 5432
Add a user
scripts/create_user.shconnects to the compose Postgres container and creates a user with a given username, printing an API key.scripts/update_user.shrotates the API key for an existing username.scripts/set_user_role.shsets a user's role toAdminorUser.- There's no signup endpoint — users are provisioned out-of-band via these scripts.
Set up the database
- Create a PostgreSQL database.
- Update the connection string in
ClipViewer.API/appsettings.Development.json(andClipViewer.Worker/appsettings.Development.jsonif you're also running the worker locally).
Run the backend (from the repo root)
dotnet run --project ClipViewer.API dotnet run --project ClipViewer.Worker # needs FFmpeg on PATHMigrations are applied automatically on startup (
context.Database.Migrate()inProgram.cs), so there's no separate migration step for local development.
cd clipviewer.vue
npm install
npm run devdotnet test ClipViewer.UnitTests # fully isolated, all collaborators mocked
dotnet test ClipViewer.IntegrationTests # real EF Core InMemory DbContext, file I/O, live worker loopcd clipviewer.vue
npm run test- If the API crashes after accepting an upload but before the worker claims the job, or a conversion job errors out partway through, the temp/partial output files aren't automatically garbage-collected. Use the retry button on a failed clip to reprocess it from the saved temp file — there's no scheduled cleanup of orphaned files beyond that (an acceptable tradeoff for a personal-scale service).
Run from the repo root:
dotnet ef migrations add <Name> --project ClipViewer.API --startup-project ClipViewer.API --context ApplicationDbContext
dotnet ef database update --project ClipViewer.API --startup-project ClipViewer.API --context ApplicationDbContextThe API auto-applies pending migrations on startup, so database update is mainly useful for local
inspection or rollback.
See docs/ENDPOINTS.md for example request/response payloads for the video endpoints.
I wanted an app I could self-host to hold video clips and share them. The various online solutions tend to expire videos after some time on their free tier, or take a while to process — which is fair given the compute cost, but neither is unreasonable, and together they gave me an excuse for a side project.
This project also doubled as a testbed for how far AI coding tools have come, and how I could fold them into my normal workflow rather than treating them as a novelty. Two spots in particular were weak points for me that AI tooling covered well. The Vue frontend uses Tailwind, and having a model that knows Tailwind's utility classes well meant I didn't have to spend much brainpower getting the UI to look reasonable and behave responsively. Testing was the other one — less a skill gap, more laziness. Early on I prompted Windsurf to generate tests, and it did a decent job using its understanding of the project through Cascade; not perfect, and some needed manual fixes (partly due to outdated framework knowledge), but still net useful.
Later work on this project shifted to Claude Code, mostly as a way to build up practice working with an agentic CLI tool day to day rather than a one-off prompt-and-paste workflow — driving real changes through a terminal agent, reviewing its diffs, and figuring out where to trust it versus where to step in.
This project is licensed under the MIT License — see the LICENSE file for details.