Publishes the public record of Legion Code Inc.'s anti-scam operation at macsitna.com, for the people scams target and the people investigating them.
Status: built and provisioned, not deployed. Both apps build and typecheck clean. The Neon database is live, migrated, and its access-control model has been verified adversarially; Cloudflare R2 and Turnstile are provisioned. There is no Vercel deployment, no Payload admin user yet, and no brand styling: every colour is a documented placeholder. See build-report.md for what is done and what is deliberately skipped, and
library/requirements/reports/for the security and QA findings, including the gates that must close before this is publicly reachable. Sections marked TODO below are facts nobody has decided yet, left visible rather than filled with guesses.
MACSITNA is the public-facing website for an ongoing anti-scam operation: a place to publish investigations, document active scam campaigns, and give readers something citable when they are trying to work out whether they are being defrauded.
A SvelteKit front end reads published content from a Payload CMS over REST. Both apps share one Neon Postgres database in separate schemas.
TODO: expand the MACSITNA acronym and state the operation's scope and remit in one paragraph.
TODO: the problem this site solves that existing scam-reporting resources do not. Two or three sentences, written by someone who knows the operation, not inferred from the stack.
git clone git@github.com:legioncodeinc/macsitna.git
cd macsitna
git submodule update --init --recursive
pnpm install
cp apps/web/.env.example apps/web/.env
pnpm dev:webThe site comes up on http://localhost:5173. It runs without a database or CMS: blog and project listings render an empty state instead of failing.
Prerequisites:
- Node.js 22 (pinned in
.nvmrc;nvm usepicks it up) - pnpm 9 or newer
- A Neon Postgres connection string, for anything that reads or writes data
- A Cloudflare account with access to the
macsitna.comzone, for DNS
pnpm install
cp apps/web/.env.example apps/web/.env
cp apps/cms/.env.example apps/cms/.envFill in both .env files before running anything that touches the database or the CMS. They are gitignored and must never be committed.
pnpm dev:web # SvelteKit site -> http://localhost:5173
pnpm dev:cms # Payload admin -> http://localhost:3000/admin
pnpm db:generate # generate a Drizzle migration from the schema
pnpm db:migrate # apply migrations (uses the DIRECT Neon endpoint)
pnpm cms:types # regenerate apps/cms/src/payload-types.tsOn first CMS boot against a fresh database, Payload creates its own payload schema and prompts you to create an admin user.
Configuration is per app, not global. Each app's .env.example is the authoritative list:
apps/web/.env.examplefor the SvelteKit siteapps/cms/.env.examplefor the Payload CMS
If you add a variable, add it to that app's .env.example in the same commit.
The variables worth calling out, because getting them wrong fails in confusing ways:
| Variable | App | Why it matters |
|---|---|---|
DATABASE_URL |
web | Neon pooled endpoint. Runtime queries. |
DATABASE_URL_UNPOOLED |
web | Neon direct endpoint. Migrations cannot run through the pooler. |
DATABASE_URI |
cms | Neon direct endpoint. Payload runs DDL on boot to sync its schema. |
PUBLIC_SITE_URL |
both | In the CMS this drives Payload's cors/csrf arrays. Wrong value means the admin works in a browser but every fetch from the site fails. |
PAYLOAD_API_URL |
web | Origin the site reads content from. |
SITE_WEBHOOK_SECRET |
both | Shared bearer token for CMS publish events. Must match on both sides. |
PAYLOAD_SECRET |
cms | Signs CMS auth tokens. Rotating it logs every editor out. |
R2_BUCKET_NAME |
cms | Leave empty in dev. Required in production: Vercel's filesystem is ephemeral, so uploads written to disk are lost on redeploy. |
PUBLIC_POSTHOG_KEY |
web | PostHog project API token. Analytics is the site's only product-analytics layer and is completely inert while this is empty: the SDK is never fetched and no script reaches the HTML. |
PUBLIC_POSTHOG_HOST |
web | PostHog ingestion origin. Empty means https://us.i.posthog.com. Must match the region baked into the CSP in apps/web/src/lib/csp.js; a US/EU mismatch fails as a 401 that looks like a bad token. |
PUBLIC_TURNSTILE_SITE_KEY |
web | Cloudflare Turnstile site key. Public by design. |
TURNSTILE_SECRET_KEY |
web | Turnstile server-side verification key. Secret. Never give it a PUBLIC_ prefix. Set it in Vercel's Production environment specifically: Turnstile is inert anywhere else. |
Two layers, and it is worth being precise about what they do and do not stop.
A honeypot field (website) is hidden off-screen. A bot that fills it gets a
silent fake success and nothing is stored. It must stay unconstrained in the Zod
schema: adding a length rule turns a filled honeypot into a validation error,
which returns a 400 naming the trap field and teaches the bot to skip it.
Cloudflare Turnstile runs in managed mode on the final step, verified
server-side. It fails open by design: if Cloudflare is unreachable, the
secret is wrong, or no token is present, the report is still accepted and the
failure logged. The people using this form have just been defrauded, and a
challenge that wrongly blocks one of them costs more than the spam it prevents.
Turnstile only activates when VERCEL_ENV === 'production'.
The consequence, stated plainly because it is easy to miss: a scripted POST
that sends the visible fields and nothing else defeats both layers. It carries
no honeypot value and no Turnstile token, and the tokenless path accepts. Neither
layer is a substitute for rate limiting on /report, which is still outstanding
and belongs at the Vercel WAF.
flowchart LR
Reader([Reader]) --> CF[Cloudflare DNS + CDN<br/>macsitna.com]
CF --> SK[apps/web<br/>SvelteKit 5 on Vercel]
SK -->|REST: published content| PL[apps/cms<br/>Payload 3 + Next.js on Vercel]
PL -->|payload schema| NEON[(Neon Postgres)]
SK -->|public schema| NEON
PL --> R2[(Cloudflare R2<br/>media)]
Editor([Editor]) --> PL
SK -->|report.created| HOOKS[Outbound webhooks]
PL -->|post.published| SK
Two rules hold this together:
- SvelteKit only ever talks to Payload over REST. Payload's Local API exists only inside the Next.js process; importing it from SvelteKit would pull the whole CMS into the bundle.
- One database, two schemas. Payload owns
payloadand manages it itself. The site ownspublicand manages it with Drizzle. Neither migrates the other's tables.
| Path | What it holds |
|---|---|
apps/web |
SvelteKit 5 public site: pages, blog, operations, report intake |
apps/cms |
Payload 3 admin and REST API, hosted on Next.js |
library/ |
Planning and reference docs (Library Schema v2): PRDs, IRDs, ADRs, knowledge |
projects/wiener-gate-public |
Git submodule: a separate published investigation repository |
.github/ |
CI workflow, Dependabot, CODEOWNERS, issue and PR templates |
build-report.md |
What was built, what was skipped, and the open risks |
nvm use # Node 22, from .nvmrc
pnpm install
pnpm dev:webSee CONTRIBUTING.md for branching, Conventional Commits, and the local gate to run before opening a pull request.
Styling is deliberately absent. apps/web/src/app.css defines the full design-token architecture with neutral placeholder values pending the brand kit; components carry structure and accessibility wiring but no visual design. Change the token values, not the components.
pnpm lint # prettier + eslint, both apps
pnpm check # svelte-check + tsc
pnpm test # vitestAll three pass. CI runs them as separate jobs on every push and pull request against main.
The CMS integration suite skips unless DATABASE_URI and PAYLOAD_SECRET are set, so it does not fail on a runner with no database. Point it at a disposable Neon branch to actually exercise it.
Target: two Vercel projects from this one repo (root directories apps/web and apps/cms), Neon for Postgres, Cloudflare for DNS, CDN, and R2 media. Merging to main is the release; there is no separate version tag.
TODO: link the Vercel projects and document the environment-variable promotion path. Neither app is deployed yet.
Contributions are welcome. Read CONTRIBUTING.md first.
MACSITNA publishes material about active scam operations. If your change touches a published claim about a person, company, or campaign, cite your sources in the pull request.
Do not report security vulnerabilities as public issues. See SECURITY.md.
MACSITNA is licensed under the GNU Affero General Public License v3.0.