Skip to content

Repository files navigation

MACSITNA

CI License

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.

What it is

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.

Why it exists

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.

Quick start

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:web

The 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.

Install

Prerequisites:

  • Node.js 22 (pinned in .nvmrc; nvm use picks 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.com zone, for DNS
pnpm install
cp apps/web/.env.example apps/web/.env
cp apps/cms/.env.example apps/cms/.env

Fill in both .env files before running anything that touches the database or the CMS. They are gitignored and must never be committed.

Usage

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.ts

On first CMS boot against a fresh database, Payload creates its own payload schema and prompts you to create an admin user.

Configuration

Configuration is per app, not global. Each app's .env.example is the authoritative list:

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.

Abuse protection on the report form

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.

Architecture

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
Loading

Two rules hold this together:

  1. 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.
  2. One database, two schemas. Payload owns payload and manages it itself. The site owns public and 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

Development

nvm use          # Node 22, from .nvmrc
pnpm install
pnpm dev:web

See 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.

Testing

pnpm lint    # prettier + eslint, both apps
pnpm check   # svelte-check + tsc
pnpm test    # vitest

All 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.

Deployment

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.

Contributing

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.

License

MACSITNA is licensed under the GNU Affero General Public License v3.0.

Releases

Packages

Used by

Contributors

Languages