Mattia Giambirtone 6fde688f2d
Some checks failed
Security verification / verify (push) Has been cancelled
fix: remediate security assessment findings
2026-08-28 12:38:38 +02:00
2026-08-18 19:13:48 +02:00
2026-08-18 19:12:54 +02:00

Meme Mash

Vibecoded clone of Make it Meme without the paywall bullshit.

A self-hosted, accountless multiplayer meme contest. The runnable MVP includes secure first-run setup, private/public rooms, authoritative Socket.IO gameplay, reconnectable player secrets, anonymous voting, persistent history, deterministic server rendering, and a separate render worker.

See the development status and roadmap for the verified implementation inventory, known gaps, and milestone acceptance criteria.

The Gitea security workflow runs the locked dependency audit, typecheck, test and build gates, scans the repository and built images, and emits OCI images with SBOM and maximum-mode build provenance. Production release images should additionally be signed by the deployment's registry identity before promotion.

Run with Docker

Docker Compose is the only supported production deployment method. Running the Node processes directly is intended for development and does not provide the required network isolation, secret validation, read-only filesystems, or reverse-proxy boundary.

install -m 600 .env.example .env
# Fill every blank secret with a distinct base64url value (24+ characters;
# COOKIE_SECRET must be 32+). Do not reuse credentials between roles.
pnpm verify:production-config
docker compose up --build

Open http://localhost:8080. On the first start, retrieve the token once with docker compose exec -T server sh -c 'token=$(cat /data/server-logs/admin.jsonl.setup-token); : > /data/server-logs/admin.jsonl.setup-token; printf "%s\\n" "$token"', then open /setup. The token is never written to service or Docker logs. PostgreSQL, media, retained service logs, and their bounded source streams use named volumes; the Valkey datastore remains intentionally disposable. The Compose entrypoint binds to 127.0.0.1:8080 by default. For a separate trusted TLS proxy, set WEB_BIND_ADDRESS to an explicit private address and port such as 10.0.0.14:8080; never use 0.0.0.0 on an untrusted network.

Fresh installations intentionally start with an empty template library. After setup, create a pack and upload the artwork you want to use before creating the first room. Upgrades do not silently delete existing database content; administrators can delete the former Original Chaos pack if it is still present and unused.

The web client is installable as a PWA on localhost and when served through HTTPS. Production deployments must terminate TLS in front of the Compose stack for browsers to offer installation. The service worker caches only the branded application shell; joining rooms and playing still require a live connection.

Run the disposable production-stack restart acceptance suite with pnpm verify:compose. It builds an isolated Compose project on a random loopback port, creates fresh volumes and three private test templates, interrupts the server during creation and voting, interrupts the render worker with pending jobs, verifies reconnect state, deadlines, partial ballots, normalized history, and rematches, then removes only that project's containers, local image tags, and volumes. The suite requires Docker Compose and may take several minutes. pnpm --filter @meme-contest/web acceptance:restart remains available for an explicitly prepared disposable Compose instance; never point it at production because it stops services and creates then deletes fixtures.

Reset a local admin password:

docker compose exec -it server node dist/reset-admin.js USERNAME
# Automation only: printf '%s\n' "$NEW_PASSWORD" | docker compose exec -T server node dist/reset-admin.js USERNAME --password-stdin

Upgrade an existing Compose installation

These steps assume the instance already has migrations 0000 through 0006. If it is older, apply every missing numbered migration in order through 0014. Migration files are not idempotent, so each must be applied exactly once.

For the security-remediation rollout, the supported helper automates the procedure below:

./scripts/deploy-security-upgrade.sh

It preserves the current COOKIE_SECRET, privately backs up .env, generates missing role credentials, builds images before downtime, stops application writes, creates and verifies a database/media/log backup, applies only absent migrations 00070014, provisions and tests the database roles, runs docker compose down without -v, and waits for the replacement stack to become healthy. It aborts rather than guessing if the current cookie secret or database administrator cannot be identified. Use --yes only after reviewing the script for unattended operation. Install deploy/nginx-outer.conf.example separately on the internet-facing proxy and run nginx -t before reloading it.

Automatic application updates are not currently implemented. The manual procedure below remains authoritative; the proposed Gitea release, migration-runner, and host-updater design is recorded in docs/SELF_UPDATE_PLAN.md for later implementation.

  1. Pull the new source, configure distinct owner/server database credentials and distinct server/worker Valkey credentials in the private .env, and build the replacement images while the current instance is still serving traffic. Keep the existing COOKIE_SECRET unchanged because it protects persisted MFA and archive material. The running containers retain their current environment until they are recreated:

    git pull --ff-only
    docker compose build postgres valkey server worker web log-maintenance
    
  2. Stop application writes, leaving PostgreSQL running, and create database, media, and retained-log backups:

    docker compose stop web server worker
    docker compose run --rm --no-deps --user 70:1000 --entrypoint sh postgres -c \
      'chgrp -R 1000 /data/postgres-logs && chmod 2770 /data/postgres-logs && find /data/postgres-logs -type f -exec chmod 0660 {} +'
    DATABASE_ADMIN_ROLE=meme ./scripts/backup.sh
    
  3. Confirm whether the reporting, submission-timing, player-activity, and asset-filename migrations have already been applied. Missing rows mean the corresponding migrations are still required:

    docker compose exec -T postgres psql -U meme -d meme -tAc \
      "select table_name from information_schema.tables where table_schema='public' and table_name in ('template_reports','removed_media_keys','template_usage_stats','template_layout_usage_stats','player_activity_entries') order by table_name"
    docker compose exec -T postgres psql -U meme -d meme -tAc \
      "select table_name||'.'||column_name from information_schema.columns where table_schema='public' and (table_name,column_name) in (('assets','original_filename'),('game_assignments','assigned_at'),('submissions','submitted_at')) order by table_name,column_name"
    
  4. Apply each missing migration once, in order:

    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0007_tearful_korvac.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0008_hard_echo.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0009_security_controls.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0010_dusty_bucky.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0011_aspiring_skreet.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0012_military_gamma_corps.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0013_condemned_mastermind.sql
    docker compose exec -T postgres psql -1 -v ON_ERROR_STOP=1 -U meme -d meme \
      < apps/server/drizzle/0014_cute_pet_avengers.sql
    
  5. Recreate PostgreSQL with the already-configured role passwords, then provision least-privilege roles through SQL stdin:

    docker compose up -d --no-deps --force-recreate postgres
    docker compose exec -T -e DATABASE_ADMIN_ROLE=meme postgres /docker-entrypoint-initdb.d/999-provision-roles.sh
    

    This transfers ownership from the legacy meme role and disables its login. It also retires the former meme_worker database login because render results are now validated and committed by the server. Verify that meme_server cannot create tables and that meme_worker has NOLOGIN before continuing.

  6. Start the updated services and verify readiness:

    docker compose up -d postgres valkey server worker web
    docker compose ps
    curl --fail http://localhost:8080/ready
    curl --fail http://localhost:8080/api/openapi.json >/dev/null
    
  7. Sign in to the administrator dashboard and verify gameplay, signed media, administrator step-up actions, and unified logs.

Production requires distinct strong POSTGRES_OWNER_PASSWORD, SERVER_DATABASE_PASSWORD, REDIS_SERVER_PASSWORD, REDIS_WORKER_PASSWORD, and COOKIE_SECRET values. Run pnpm verify:production-config before Compose changes; it rejects exposed .env permissions, reused credentials, unsafe origins/binds, and an invalid Compose model. Known examples, low-diversity credentials, and a production DATABASE_URL using the legacy meme role are rejected. Because application containers run unprivileged, ensure existing media/log volumes have the ownership documented by Compose. Migration 0014 adds only the partial unpublished-outbox index. If rollback is necessary, rebuild the prior images; do not reverse migrations except by restoring the verified backup.

PostgreSQL requires its initial cluster bootstrap role to retain superuser status. Fresh volumes use meme_owner for that offline owner role; upgraded volumes retain the former meme bootstrap role with login disabled. The server alone connects as restricted meme_server; the render worker has no database credentials and can submit only versioned results through its restricted Valkey ACL user.

Local development

Node 24+ and pnpm 10 are expected. Start PostgreSQL and Valkey, copy .env.example to .env, set PUBLIC_ORIGIN=http://localhost:5173, then run pnpm install, pnpm db:generate, and pnpm dev. API documentation is exposed at http://localhost:3000/docs; readiness is at /ready. Metrics are public only in development; production requires a bearer token configured with METRICS_TOKEN.

Architecture and guarantees

  • apps/web: Svelte 5/Vite mobile client; room-scoped credentials live in IndexedDB.
  • apps/server: Fastify REST, admin security, Socket.IO authority, PostgreSQL revision/outbox persistence, Valkey fan-out.
  • apps/worker: BullMQ/Sharp deterministic PNG and animated-WebP rendering with three retries.
  • packages/shared: Zod contracts, scoring, ranking, phases, and command protocol.
  • packages/renderer: deterministic plain-text layout and SVG generation used by canonical renders.

Every command carries an idempotency key and expected revision. Secrets are random and only SHA-256 hashes reach PostgreSQL. Admin passwords use Argon2id; session and CSRF tokens are also stored hashed. Uploads reject SVG/animation and enforce administrator-configured source limits (15 MB and 4096×4096 by default, bounded at 100 MB and 8192×8192).

Maintenance mode

Administrators with operations-management permission can enable maintenance mode from the Operations & Safety panel. Graceful maintenance blocks new rooms, joins, discovery, archives, reports, and other public APIs while allowing already-started games to reconnect, load media, and finish; waiting lobbies are suspended without being deleted. Forced maintenance additionally expires every nonterminal room, records partial games in normalized history, and disconnects all players. Disabling maintenance restores public access but does not reopen rooms ended by a forced shutdown. The selected mode and player-facing notice are stored in PostgreSQL and restored after a server restart.

Administrator service logs

Administrators with the explicit LOGS_VIEW permission can inspect recent structured entries from the application, render worker, Nginx, PostgreSQL, and Valkey in one dashboard, filter by service, level, or indexed text search, choose case-sensitive or case-insensitive matching, and enable or disable live Socket.IO streaming. Administrators who also have OPERATIONS_MANAGE can set the unified retained maximum from 100 to 10,000 entries; the default is 1,000. Each server process has a random instance ID and start time, so the browser can report server restarts without granting access to the Docker socket.

The retained administrator history, its in-memory search index, and per-source cursors are rebuilt from atomically persisted data after restarts. All raw service-log mounts are read-only in the internet-facing server. A network-isolated, unprivileged log-maintenance sidecar alone truncates bounded active files and removes expired raw rotations. Expanded entries retain useful redacted request, client, browser, worker-job, and database-process metadata. Safe Nginx query strings are retained; any query containing a credential/capability-shaped parameter is redacted before Nginx writes it. Cookies, authorization, passwords, tokens, signatures, MFA material, PostgreSQL statements, and parameters remain excluded or redacted.

Administrative audit entries use the same redaction rules and include an explicit outcome plus request ID, route, method, network/browser context, process instance, and actor permissions. Failed password or MFA sign-ins, permission denials, CSRF/origin rejection, and failed or rate-limited password step-up attempts are recorded without storing submitted credentials or CSRF/capability values.

The logging directives in deploy/nginx.conf belong to the internal Compose Nginx container. The exposed TLS reverse proxy must use the query-free access-log format and global request ceiling in deploy/nginx-outer.conf.example, rotate its files by size and age, continue proxying the complete site to the configured WEB_BIND_ADDRESS, preserve WebSocket upgrades for /socket.io/, and overwrite rather than append untrusted X-Forwarded-For and X-Forwarded-Proto headers as described in docs/SECURITY_REMEDIATION.md. Do not expose outer-proxy logs to the application.

Administrator analytics

Administrators with ANALYTICS_VIEW can inspect aggregate gameplay and content performance over the last 7, 30, 90, or 365 days. The dashboard reports game completion and rematches, participation and timing, connection churn, and pack/template presentation, submission, skip, and vote trends. It deliberately avoids player names, captions, IP addresses, browser details, and cross-room identity; accountless players are not treated as stable users.

Backups and retention

Run ./scripts/backup.sh [destination] to create a private timestamped backup. The script records which application services are running, stops them to quiesce database and media writes, verifies every database-referenced blob against the media archive, and restores only the previously running services even after failure or interruption. It verifies the PostgreSQL custom archive, media/log tar archives, blob manifest, deployment-secret fingerprint, SHA-256 manifest, and a manifest HMAC made with COOKIE_SECRET before atomically publishing the directory. It requires at least 1 GiB free by default and retains the newest 14 backups; override those safeguards with BACKUP_MIN_FREE_MEGABYTES and BACKUP_RETENTION_COUNT. Copy verified backups to encrypted off-host storage; a host-local copy is not disaster recovery.

Validate restore in a separate disposable Compose project and volumes: first compare the SHA-256 fingerprint of the escrowed production COOKIE_SECRET with secret-fingerprints.txt, recompute SHA256SUMS.hmac with that secret, then run sha256sum -c SHA256SUMS. Restore database.dump with pg_restore --clean --if-exists, extract media.tgz into the offline media volume, and extract logs.tgz with an offline recovery container mounting only the destination log volumes. A backup is not recoverable without the separately escrowed COOKIE_SECRET; test that escrow during every restore drill. Do not make the running server's raw-log mounts writable for restore. Start the isolated stack and inspect an archived game and its media; never test restore over production volumes. Valkey data remains disposable.

Compose mounts migrations for initialization of new database volumes; PostgreSQL does not replay initialization scripts for an existing volume. Follow the upgrade procedure above instead.

Scope

This repository implements the playable MVP foundation, including pack administration, a freeform template-region editor, QR sharing, automatic phase transitions, audio, an installable PWA shell, and privacy-safe aggregate analytics. Analytics export, durable operational time series, and load-test harnesses remain follow-up work; their persistence and protocol boundaries are represented so they do not require a redesign.

Description
Vibecoded clone of Make it Meme without the paywall bullshit
Readme Apache-2.0 891 KiB
Languages
TypeScript 59.2%
Svelte 26.8%
CSS 9.3%
Shell 2.6%
JavaScript 1.8%
Other 0.2%