CTCarl Tracy
All work
2026

Stackyard

A video platform nobody else can switch off.

not deployed yet

In progress — no preview yet

Not built yet.

Why not just use YouTube

Because platforms delete things, demonetise things, and change the rules without asking. None of those levers exist on a server Carl controls — which is the entire reason this exists, and the reason every other decision below follows from it.

Stackyard is a complete, working alternative: upload, a paywall that actually gates the file rather than just hiding a link, a site-wide chat room, playlists, RSS, a studio dashboard. Three dependencies — Express, Multer, bcrypt — and roughly 5,200 lines of code that do the rest.

What’s actually been verified

scripts/smoke.js runs 71 end-to-end checks against the running app: every public route, upload through to a playable file, Range and suffix-Range streaming, the members wall tested from both an anonymous session and a signed-in-but-not-paying one, access-code redemption and its refusal on reuse, CSRF on forms and the JSON API, path traversal attempts on the media and thumbnail routes, chat rate limiting, comment and chat XSS escaping, and catalogue export. All of it passes, and reruns clean against a database that already has old test data sitting in it — the harder case, and the one that actually matters.

What it is not, yet

Never deployed. No domain, no TLS, no server outside this machine. No real video has been uploaded — only clips ffmpeg generated for testing. Donation links are empty and the studio dashboard says so on its own front page rather than waiting to be asked. No email is ever sent; the contact inbox and subscriber list just collect addresses.

None of that is hidden in the write-up, because a working thing that admits what is left is more useful than a finished-looking thing that doesn’t.

The one correction that mattered most

Every design choice here started from YouTube’s layout and then deliberately un-became it. The clearest moment was Carl’s own mid-build note, verbatim: “square it off… make the main color rather than red, make it white, and where the play symbol is have a video icon.” Red survives nowhere except the colour reserved for destructive actions. The wordmark is a white square with a video-camera glyph in it rather than a play triangle. Nothing about this reads as a familiar platform, which was the point of building it at all.

Refinements

How it changed

Every pass, dated, oldest first — nothing trimmed and nothing tidied out. The version that got rejected is usually more informative than the one that shipped.

10 entries·scroll sideways

10

  1. Started
    Self-hosted, because that was the entire point

    Three platforms were on the table; self-hosting on his own server won because it is the only one where nobody else can switch it off. Cloudflare Stream would have been less work and would have reintroduced exactly the third party this exists to avoid. The consequence accepted up front: backups and bandwidth are now his problem, not a platform's.

    Functionality
  2. Changed
    Donation links and access codes, not a payment processor

    No Stripe in the first pass. The site stores no payment details, carries no PCI surface, and nothing that can keep charging someone after the fact — at the cost of a manual step per member. The database already treats a membership expiry date as the single source of truth, so adding Stripe later is one webhook calling the same code-granting path, not a rewrite.

    Functionality
  3. Fixed
    "Square it off" — correcting course mid-build

    His words, after seeing the first pass: "that a bit too youtube like. layout is good though… the rounded effect is a bit too much youtube. square it off, the search bar and the youtube button, make the main color rather than red, make it white, and where the play symbol is have a video icon." Radii dropped from 12–40px pills to 3–4px, the accent went from red to white with dark text on it, red survives only as the colour for destructive buttons, and the wordmark became a white square holding a video-camera glyph rather than a play triangle.

    Style
  4. Changed
    A chat room, separate from comments

    His addition, mid-build. Comments stay per-video and about that video; /chat is one site-wide room for everything else — somewhere for an audience to be that is not a platform.

    Functionality
  5. Changed
    Two registers in one app

    The grid and the player use app vocabulary, because that is what makes a video site legible. About, Donate, Terms and Privacy switch to a display serif, a generous measure and hairline rules. The split exists so the prose pages can read as considered without making the player look like a magazine.

    Style
  6. Note
    One file, original quality — no transcoding ladder

    A renditions ladder triples storage and costs CPU for a problem that does not exist yet. One file is served at upload quality, with a lossless faststart remux so seeking works immediately. The right call only once bandwidth becomes the actual bottleneck, and not before.

    Quality of life
  7. Note
    The view counter stores no IP addresses

    A per-day counter plus a 24-hour cookie — enough to count a view once, nothing that identifies who made it. The privacy page says exactly this, and it is true, which is the only version of a privacy page worth shipping.

    Quality of life
  8. Fixed
    An icon rendered 471px wide

    Inline SVG icons had no base size rule, so one dropped into running text filled the screen. Fixed with a single svg { width: 1.15em } default that every sized context overrides — the kind of bug that is invisible until the exact moment it is not.

    Functionality
  9. Fixed
    The test that revoked the wrong code

    The smoke test picked which access code to clean up with .at(-1) on a newest-first list — the oldest row, not the one it had just created. Harmless with one row in the table, so it passed every early run and only surfaced once codes accumulated, at which point it silently revoked an unrelated code instead of its own. Fixed to .at(0), and the cleanup step now actually revokes its own code rather than leaving it to accumulate. Rerun twice against a table with leftover revoked codes already in it, to confirm it holds.

    Functionality
  10. Shipped
    71 end-to-end checks, and an honest account of what is left

    Every public route, the paywall from both an anonymous and a signed-in-but-not-a-member session, CSRF on forms and the JSON API, path traversal on the media routes, chat rate limiting, XSS escaping, catalogue export — all scripted and passing. Equally recorded without softening: never deployed, no real video uploaded, donation links empty, no email ever sent, only tested in Chromium. The second list is as important as the first.

    Quality of life

Screens

A look around

Look around — 4 screens

  • Stackyard: The grid. Squared geometry, white accent, a members badge on the paywalled clip — and video titles that admit this is a testing ground.
    The grid. Squared geometry, white accent, a members badge on the paywalled clip — and video titles that admit this is a testing ground.
  • Stackyard: The watch page. Range-request streaming, chapters, an up-next rail — no recommendation engine behind it, by design.
    The watch page. Range-request streaming, chapters, an up-next rail — no recommendation engine behind it, by design.
  • Stackyard: The About page in its editorial register — display serif, generous measure — deliberately different from the app chrome around it.
    The About page in its editorial register — display serif, generous measure — deliberately different from the app chrome around it.
  • Stackyard: The studio dashboard. A sparkline, storage used, and a "needs attention" list that says plainly when donation links are not set up yet.
    The studio dashboard. A sparkline, storage used, and a "needs attention" list that says plainly when donation links are not set up yet.