Skip to content
notBscalEPublic

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Videonim

Videonim is a personal board for saving and tagging X (Twitter) videos. Each user has a private board and can:

  • Save an X/Twitter status/video link — Videonim validates it, parses the author and tweet id out of the URL, and stores it.
  • Embed & watch the video inline, and copy the canonical post link.
  • Tag links (Unicode incl. Hebrew, spaces, _/-; up to 20 tags / 30 chars), then search the board by author or tag (case-insensitive).
  • Edit tags and delete links.

Boards are private (JWT auth), duplicates are prevented per user (keyed on tweet_id), and only valid X/Twitter links are accepted.


Directory structure & components

npm-workspaces monorepo. Two apps (server, web) and two shared libraries (x-url, contracts) that are published to JFrog Fly as standalone artifacts:

.
├── package.json                 # workspaces: packages/*  + build/test/typecheck scripts
├── docker-compose.yml           # local stack: db + server + web
├── db/init.sql                  # MySQL schema bootstrap (+ isolated videonim_test DB)
├── .github/workflows/ci.yml     # CI: test → build/scan/push images → publish npm to Fly
└── packages/
    ├── x-url/                   # @videonim/x-url — pure X-URL parsing → {author, tweet_id}, zero deps
    ├── contracts/               # @videonim/contracts — zod request schemas + tag rules (zod only)
    ├── server/                  # @videonim/server — Node + TS API (Express, MySQL); bundles the libs
    │   ├── src/                 # config, db, security, routes, serializers, app, index (bin)
    │   ├── config/              # per-env YAML: local | test | preprod | prod
    │   ├── Dockerfile           # multi-stage; builds from the REPO ROOT (monorepo bundling)
    │   └── test/                # Vitest + supertest (needs MySQL)
    └── web/                     # @videonim/web — React (Vite) SPA served by nginx
        ├── src/                 # components, auth context, api wrapper, twitter widget loader
        ├── Dockerfile           # node build stage → nginx-unprivileged runtime
        └── test/                # Vitest + React Testing Library (no services needed)

Components

Component Tech Role
web React (Vite) + nginx SPA UI; nginx serves it and reverse-proxies /api → server
server Node + TypeScript (Express) REST API, JWT auth, validates/parses X links, MySQL access
db MySQL 8 Stores users and links (local container; external in deployed envs)
@videonim/x-url TypeScript (tsup) Pure X/Twitter URL parser — reused by server
@videonim/contracts TypeScript + zod Shared validation schemas, tag normalization, request types
browser ──▶ web (nginx :8080) ──/api──▶ server (:8000) ──▶ db (MySQL :3306)

Shared libs are a future-split artifact, not a runtime dependency. The server imports @videonim/x-url / @videonim/contracts from local workspace source and tsup bundles them into its image — it declares no @videonim/* deps and never pulls the published packages at build/run time. They're published to Fly so the monorepo can later split into separate services.

Using Fly

The project is wired to JFrog Fly as its artifact registry, release tracker, and deployment-visibility layer (Fly does not replace Git or run deployments itself):

  • Registry — CI publishes the Docker images (videonim-server, videonim-web) and the npm packages (@videonim/x-url, @videonim/contracts, @videonim/server) to the Fly registry. Base images can also be pulled through Fly (see FLY_REGISTRY).
  • CI auth — jfrog/fly-action@v1 authenticates with GitHub OIDC (no token in the repo) and points Docker/npm at the Fly registry.
  • Releases — every main build becomes a traceable Fly release (commits, PRs, artifacts, AI summary), versioned <major>.<minor>.<run_number>.
  • Runtime environments — deployments are tracked per environment (test, prod, …) when a cluster pulls images using a Fly environment-scoped pull secret (fly-<env>-registry-secret), so Fly shows what's running where.
  • Decision records (ACT) — agent decisions are captured into the gitignored .fly/agent-trace-decision-records.md as shared, repo-scoped project memory.
  • Security gate — images are CVE-scanned in CI with Trivy (fails on HIGH/CRITICAL).

Data model

The data is saved in a regular MySQL database, in these two tables:

  • users: id, username (unique), password_hash, created_at
  • links: id, user_id → FK to users (ON DELETE CASCADE), url, author (own indexed column), tweet_id, tags (JSON array), created_at. A (user_id, tweet_id) unique key enforces per-board de-duplication.

Configuration & secrets

Active environment comes from ENVIRONMENT (empty → local); non-secret settings live in packages/server/config/<env>.yaml. Secrets come only from env vars — the server fails fast if a required one is missing.

The exact variables you set depend on how you run the project — see the per-setup tables in Running it locally: one for Docker Compose and one for local builds & tests. (Deployed environments additionally accept DB_HOST / DB_PORT overrides to point the server at an external database.)


Running it locally

Option A — Docker Compose (full stack)

Prerequisites: Docker + Docker Compose.

cp .env.example .env          # fill in secrets — e.g. JWT_SECRET via: openssl rand -hex 32
docker compose up --build     # builds & starts db → server → web
# open http://localhost:8080
curl http://localhost:8080/api/healthz   # {"status":"ok","environment":"local"}

docker compose down           # stop (keeps data)
docker compose down -v        # stop and wipe the MySQL volume

Variables — all set in .env. The first three are required (compose refuses to start without them); the rest are optional. Note the Type column: every variable here is a runtime environment variable read by the containers, except FLY_REGISTRY, which only affects the image build.

Variable Type Required Default Purpose
DB_PASSWORD runtime env 🔒 yes — App DB user password (server + MySQL)
DB_ROOT_PASSWORD runtime env 🔒 yes — MySQL root password (db container)
JWT_SECRET runtime env 🔒 yes — Signs / verifies JWT auth tokens (server)
ENVIRONMENT runtime env no local Selects config/<env>.yaml
DB_NAME runtime env no videonim Database name
DB_USER runtime env no videonim Database user
FLY_REGISTRY build-time no empty → Docker Hub Base-image registry prefix (trailing slash), e.g. <host>/docker/; used for ${…} substitution + the REGISTRY build arg

🔒 = secret. (DB_HOST/DB_PORT are fixed to db/3306 by compose, so you don't set them here.)

Note: the server image builds from the repo root (it bundles the workspace libs), which docker-compose.yml handles via context: . + dockerfile: packages/server/Dockerfile.

Option B — local builds & tests (Node 20+, 24 recommended)

npm install                   # installs all workspaces
npm run build                 # builds libs first, then server (tsup) + web (vite)
npm run typecheck             # libs + server
npm test                      # x-url + contracts + server + web (Vitest)

Variables — npm install / build / typecheck and the x-url / contracts / web test suites need none. Only the server test suite needs MySQL + the vars below (all runtime environment variables — no build-time variables in this path):

Variable Type Required Default Purpose
DB_PASSWORD runtime env 🔒 yes — Password for your local MySQL
DB_HOST runtime env recommended per-env yaml MySQL host — set 127.0.0.1 for host runs
DB_USER runtime env recommended videonim MySQL user — set root if connecting as root
DB_NAME runtime env no videonim_test (forced by the test harness) Isolated test database
JWT_SECRET runtime env no throwaway test default Signs JWTs during tests
ENVIRONMENT runtime env no local Selects config/<env>.yaml

🔒 = secret. Start MySQL first, then run with those vars:

docker compose up -d db
DB_HOST=127.0.0.1 DB_USER=root DB_PASSWORD=<root-pw> DB_NAME=videonim_test \
JWT_SECRET=test-secret ENVIRONMENT=local npm run test -w @videonim/server

No Node on your machine? Run the whole thing in a node:24 container, with a MySQL container on the same Docker network — npm install && npm test.

API

All endpoints under /api; protected ones require Authorization: Bearer <token>.

Method Path Auth Description
GET /healthz no Liveness probe
POST /auth/register no Create a user
POST /auth/login no Get a JWT
GET /auth/me yes Current user
GET /links?q=<term> yes List your links; q filters author/tag
POST /links yes Save a link ({url, tags[]})
PATCH /links/{id} yes Update a link's tags
DELETE /links/{id} yes Delete one of your links

Run the CI

CI is GitHub Actions (.github/workflows/ci.yml), integrated with Fly. It builds, scans, and publishes artifacts to JFrog Fly — it does not deploy to any cluster. Authentication is OIDC via jfrog/fly-action@v1 (the workflow has permissions: id-token: write); no Fly token is stored in the repo.

Four jobs:

  1. changes (always) — detects whether a non-README file under the watched paths changed. If only README.md files (or nothing relevant) changed, the jobs below are skipped (so a skipped-but-required test check still lets README-only PRs merge).
  2. test (when relevant files changed) — MySQL service container; npm ci, npm run typecheck, npm test (all four packages).
  3. build_scan_push (main only) — builds the videonim-server (root context) and videonim-web images, scans them with Trivy (fails on HIGH/CRITICAL, --ignore-unfixed), then pushes both to the Fly Docker registry tagged with the commit SHA and latest.
  4. publish_package (main only) — builds the libs + server and publishes @videonim/x-url, @videonim/contracts, @videonim/server to the Fly npm registry. jfrog/fly-action@v1 configures the runner's .npmrc (registry + OIDC token), so npm publish resolves to Fly. Versions are CI-derived: <major>.<minor>.<run_number> (bump major/minor in package.json).

Do not remove jfrog/fly-action@v1 — it is the registry + auth setup for both Docker and npm. Without it, pushes/publishes go nowhere (or to public npm).

What a Helm chart + ArgoCD would need (deployment is illustrative, not in this repo)

CI only publishes artifacts; ArgoCD would be the deployer and Fly would track the result. For a chart driven by ArgoCD to run and be tracked by Fly:

  • Images reference the Fly registry — <your-fly-subdomain>.jfrog.io/docker/videonim-server:<tag> and …/videonim-web:<tag> (pin to the commit-SHA tag for prod; :latest is fine for test). The registry host isn't hardcoded in the repo — base images use a REGISTRY build arg (FLY_REGISTRY in compose), defaulting to public Docker Hub.
  • A Fly environment-scoped image pull secret exists in the target namespace and is referenced via imagePullSecrets. Create it out-of-band per environment (fly-<env>-registry-secret, e.g. fly-test-registry-secret) — ArgoCD/Fly do not create it for you, and the matching Fly runtime environment must already exist. When the cluster makes the authenticated pull with that token, Fly records the deployment under that environment. (imagePullPolicy: Always is recommended so each rollout triggers a fresh, tracked pull that Fly records.)
  • Config & secrets: a ConfigMap for non-secret env (ENVIRONMENT, DB_*), and a Secret for JWT_SECRET / DB_PASSWORD created out-of-band (never committed).
  • Stable service names server and web — the web nginx proxies /api/ to http://server:8000, so the backend Service must be named server.
  • A database — an in-cluster MySQL (local/test) or an external DB via the DB_HOST/DB_PASSWORD overrides.
  • ArgoCD specifics: ArgoCD reads the chart from Git, so the chart must be committed and ArgoCD given repo credentials (this repo is private). An ArgoCD Application points at the chart path, target revision (branch/tag), and destination namespace, and syncs it.

Security notes

  • Passwords hashed with bcrypt (bcryptjs); generic auth errors (no user enumeration).
  • JWT (HS256), configurable expiry, secret from env only.
  • Parameterized queries only (mysql2) — no SQL injection, including tag search.
  • Strict zod input validation (URL pattern, length caps, tag count/length).
  • Every link query is scoped to the current user; create/update/delete check ownership (no IDOR).
  • nginx sends security headers / CSP (allows only the X embed widget's hosts).
  • Containers run non-root; base images are security-patched; .env/secrets stay out of images and git.

Not yet

  • No Terraform / cloud infra — local-first; CI publishes to Fly but does not deploy.
  • Auth is username + password; linking X/OAuth accounts is planned.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages