Lossless Changelog
fleet

Papermark Goes Live — the First Self-Hosted Data Room, Wrestled Out of a Source Tree That Doesn't Want to Be Self-Hosted

the-water-foundation now has a working Papermark VDR — the first in the stack. Getting there meant making an OSS repo build that imports 41 closed-source Enterprise modules it doesn't ship, then chasing a 404 through three middleware red herrings to the real culprit: the webhook host set equal to the app host.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 4.8

Why Care?

Papermark has sat on the README's VDR roster (DealRoom $12k–15k/yr, ShareVault $1.5k–5k/deal → $0) since the beginning, but unlike Twenty, Outline, and Postiz it had never actually been stood up — no proven runbook, only a draft spec. Today it went from roster row to running software: the-water-foundation has a self-hosted virtual data room at https://papermark-production-722f.up.railway.app, login page live, auth redirect verified. It's the first Papermark instance in the stack, and because every fix landed in our fork, the next client's is a repeat, not a re-fight.

What's New?

  • Papermark deployed into the the-water-foundation Railway project: papermark (built from the lossless-group/papermark fork), plus its own papermark-postgres (16) and a papermark-storage Railway bucket (S3 via NEXT_PRIVATE_UPLOAD_*). Core VDR — document upload, share links, data rooms, page analytics, NextAuth — intact.

  • Added to the client portal — hubs/lossless-at/src/config/clients.ts now carries dataroom_url for the-water-foundation, so the homebase at lossless.at/the-water-foundation surfaces a "Your Data Room" card alongside CRM and Knowledge Base.

The Fight — making OSS Papermark build from source

Papermark's public main imports 41 closed-source Enterprise (ee/*) modules — branding, redaction, conversations, request-lists, confidential-view, AI, premium limits — plus a couple of core files (lib/oauth/scopes, lib/api/errors) that simply aren't in the OSS tree. next build dies on module resolution. The repo isn't designed to be built from source by outsiders. We made it build anyway, all committed to the fork:

  1. railway.json + .railpackignore — force Nixpacks to build Node, not Python (the repo's Tinybird Pipfile fools Railpack). Build command npm install --include=dev because NODE_ENV=production otherwise skips the devDependencies next build needs (e.g. @tailwindcss/forms).

  2. 41 auto-generated stubs — a codegen pass over every unresolved @/ee / @/lib/oauth import (static, dynamic, and re-export forms) emitting a universal callable/constructable proxy so new X() and <X/> both survive. The EE features are inert at runtime; the core VDR is whole.

  3. Typecheck + ESLint off at build, @react-email/components added (imported by ee/ email templates, undeclared upstream), and 5 EE branding/preview pages neutralized because they can't prerender without EE data.

  4. ~32 dummy integration keys (OpenAI, HANKO, Slack, Jitsu, QStash, Trigger, Upstash, Stripe, …) so eager client constructors don't throw at module load during build and boot.

The build went green and the app booted — migrations applied, Next.js ready.

The 404 — three red herrings and the real bug

The app booted but every page 404'd (/dashboard, /login, /register) while /api/health returned 200. The x-middleware-rewrite: /404 header said the middleware was rewriting everything. Three plausible-but-wrong fixes went by first (all kept, all harmless, none the cause):

  • treat NEXT_PUBLIC_APP_BASE_HOST as the app domain in isCustomDomain,

  • hard-allow *.up.railway.app in the platform-host list,

  • resolve x-forwarded-host (the raw Host differs behind Railway's proxy).

The actual culprit: Papermark's middleware runs isWebhookPath(host) before the app router, and isWebhookPath returns true when host === NEXT_PUBLIC_WEBHOOK_BASE_HOST. We'd set WEBHOOK_BASE_HOST to the app's own domain (it needed a non-empty value to satisfy a next.config header rule at build), so every request matched → routed to the webhook middleware → rewritten to /404. Setting WEBHOOK_BASE_HOST to a distinct dummy that never equals a real request host fixed it instantly. Login page renders; unauth /dashboard → 302 /login?next=/dashboard.

Gotchas (for the runbook this becomes)

  • NEXT_PUBLIC_WEBHOOK_BASE_HOST must NOT equal the app host — it needs a non-empty value for the build, but any value equal to a real request host routes all traffic into the webhook middleware and 404s the app.

  • NEXT_PUBLIC_* is build-time inlined into edge middleware — changing one requires a rebuild, not just a restart.

  • NODE_ENV=production skips devDeps on npm install — force --include=dev in the build command or next build fails on missing build-only packages.

  • Fork the EE-stub set forward — a fresh upstream sync re-drops the ee/* files; re-run the stub codegen after syncing.

What's Left

Login works, but first sign-in needs an auth method — a Resend API key (magic links) and/or Google OAuth (the client runs Google Workspace) — both operator-provided. Then the first admin account, then palmer-ai's Papermark as a fast repeat off the same fork.