Skip to main content

How dose.wiki is built

The public site, editor, data pipelines, and database.

dose.wiki is a harm-reduction encyclopedia of substances, subjective effects, and trip reports. Three systems share its PlanetScale Postgres database: a public website that displays content and accepts reader submissions, a protected /dev editor for writing, review, and citations, and command-line pipelines that scrape, generate, and migrate content.

01 The big picture in numbers

267
Published substance articles
233
Subjective effects
272
Trip reports
~1,400
TS/TSX files in src/
~700
Pipeline scripts in scripts/

Pipelines produce most of the site's content and media. Reading only src/ shows the website, but not the tooling that fills it.

02 System overview

Scrapers and AI workflows supply content; readers submit reports and feedback. Shared server functions connect these inputs and the editor to the database. The public site serves published records, with replication files delivered separately from Cloudflare R2.

Public site
/dev editor
Database and server functions
Scripts & pipelines
External / AI services
Sources and OpenRouter feed scripts that write to PlanetScale Postgres and upload media to Cloudflare R2. Next.js reads the database and serves the public site and gated /dev editor. Browser submissions and editor saves pass through app API routes; R2 supplies public media.
There is no separate REST service. TypeScript queries (reads) and mutations (writes) in server/*.ts run inside the Next.js app and scripts through the Postgres adapter in lib/postgres/runtime/. Browser saves and submissions enter through Next.js route handlers in src/app/api/; access controls differ by route.

03 Tech stack: what each tool is for

Framework
Next.js 16 + React 19

App Router server components fetch data and render mostly static HTML, prerendered at build time where possible. Vercel hosts the app.

Database + backend
PlanetScale Postgres

PlanetScale Postgres hosts the production database. Authored queries, mutations, and schema definitions live in server/; lib/postgres/runtime/ executes them inside database transactions.

Auth
next-auth: username and password

auth.ts configures a username-and-password credentials provider backed by memberships. Passwords are stored as scrypt hashes, not environment-variable passwords. Middleware keeps protected routes on the editor hosts; roles determine access.

AI generation
OpenRouter

An API gateway to multiple language models. Section synthesis, dosage and duration rewrites, quote extraction, and citation workflows each select a model in their configuration or entry module. Model choices change over time.

Styling + UI
Tailwind CSS 4 + Radix UI

Tailwind provides utility-first styling from the tokens in src/theme/; Radix supplies accessible UI primitives. Framer Motion handles animation, and React Hook Form manages editor forms.

Media + abuse controls
Cloudflare R2 + Turnstile

R2 stores replication files. Turnstile adds a CAPTCHA to site feedback; other intake controls and media handling are described below.

Tooling + safety
Bun, Vitest, Zod

Bun manages packages; Vitest runs the tests (~500 test files in the recorded inventory). Zod schemas in src/schema/ and data/schemas/ reject invalid article structures before storage or rendering. Structural validation does not establish factual accuracy. Molecule diagrams use the vendored OpenChemLib fork in vendor/openchemlib, checked by scripts/chemistry/verify-openchemlib-fork.mjs.

04 Repository map: where things live

Four directories hold most code: src/ for the app, server/ for database functions, scripts/ for pipelines, and lib/ for shared server helpers. The rest holds documentation, assets, vendored dependencies, and configuration.

dose.wiki/
├── src/                      · Next.js application
│   ├── app/                  · public pages, /dev, API routes
│   ├── features/             · domain modules
│   ├── components/           · shared layouts, pages, UI primitives
│   ├── schema/               · Zod article schemas
│   ├── data/                 · copy seeds, JSON artifacts, generated maps
│   ├── theme/                · design tokens
│   └── middleware.ts         · /dev access and host policies
├── server/                   · schema.ts and table functions, executed on Postgres
├── scripts/                  · data pipelines (section 08)
│   ├── parsers/  batch/  citations/  prepopulate/
│   ├── replications/  contributors/  chemistry/  legality/
│   ├── data-ops/  migrate/  seed/  build/  deploy/  reports/
│   ├── tools/  perf/  config/  eslint-plugins/
│   └── lib/                  · CLI plumbing, OpenRouter client
├── lib/                      · app and script server helpers
│   ├── data/                 · publicData.* database readers
│   ├── next/                 · route loaders, copy blocks, host policies
│   ├── postgres/             · Drizzle schema and native callable runtime
│   └── auth/  http/  og/  citations/  runtime/
├── vendor/openchemlib/       · vendored molecule-rendering fork
├── public/                   · molecule images, flags, favicons
├── docs/                     · architecture docs, ADRs, operations and workflow guides
├── data/  content/           · datasets with licenses, and every authored prose file
└── README.md  ARCHITECTURE.md  CONTRIBUTING.md  AGENTS.md

Citation auditing and legality research run as editorial agent workflows maintained outside this repository; the runbooks they follow are docs/workflows/citations.md and docs/workflows/legality.md. Other workflow guidance lives beside its scripts or in docs/workflows/.

05 Frontend: routes and feature modules

src/app/ maps URLs to page routes and ~70 API handlers. Page routes fetch data, prepare metadata, and pass content to the rendering logic in src/features/, grouped into 15 domain modules.

src/app/ · public routes
  • /[slug]: the core substance article.
  • /substances, /effects, /effects/[effectSlug], /reports, /reports/[slug]: content catalogs and entries.
  • /replications, with audio, tutorials, artist/[key], and [slug] routes: gallery browsing and playback.
  • /reports/submit: trip-report intake. /about/feedback: site feedback; each article also has a feedback box.
  • /psychoactive, /chemical-classes, /mechanism, /category: classification indexes.
  • Other pages: /interactions, /search, /about, /contributors, /articles, /blog, /mantras, /documentation-style-guide.
  • /open-data/*.json and /api/v1: machine-readable data. Exports are linked from /about#data; /data redirects there.
  • /docs/how, /docs/code, /docs/license: article methodology, this page, and licensing.
src/app/ · protected routes
  • /dev: a catch-all page (src/app/dev/[[...segments]]) dispatches ~17 tabs: articles, writing, citations, trip-report submissions, article and site feedback, replications, molecules, copy, banners, contributors, tags, index layout, change log, and profiles.
  • /review: a standalone, full-width article-review workbench.
  • /api/dev/*: ~15 per-tool write endpoint families. /api/save-article: whole-article saves.
  • /sign-in: next-auth sign-in. Protected routes require an authorised session.

NEXT_PUBLIC_SITE_FLAVOR selects dose.wiki or Effect Index at build time. src/config/siteFlavor.ts controls their navigation, wordmarks, and available routes; both publications share content records. Public DoseWiki, editor DoseWiki, and Effect Index are separate build artifacts and deployments. Only the editor carries general admin credentials; public DoseWiki has restricted create-only intake permissions, and Effect Index is read-only.

src/features/ · public-facing (14 modules)
  • article/: dosage tables, duration charts, interaction grids, citations, and article feedback.
  • effects/: effect rendering, including the custom VCode markup.
  • reports/: report rendering and submission forms.
  • replications/: gallery exploration, media viewing, and artist showcases.
  • Ten smaller modules: blog/, articles/, chemical-classes/, coverage/, mantras/, site-feedback/, mailing-list/, effect-index/, psychoactive-summaries/, theme-lab/.
src/features/dev/ · the editor (~400 TS/TSX files, including tests)
  • forms/: one React Hook Form section per article field group.
  • tools/: ~14 tools, including substance, replication, copy, molecule, and banner editors; citation review; three intake queues; contributors; writing; and index layout.
  • save-orchestrator/: coordinates multi-endpoint saves.
  • context/, notices/, profile/, tags/, prompts/: editor state, notifications, profiles, tags, and prompts.

06 The data layer: what is in the database

server/schema.ts declares the data shapes (~45 in the recorded inventory); most tables have a matching server/<table>.ts query-and-mutation module. The groups below describe purpose, not visibility: editorial records can include public profiles, while content configuration can hold unpublished drafts. Related tables share a row. Stored-record estimates are historical snapshots, distinct from the live published counts above.

The data layer: what is in the database
TableWhat it holds
Content and configuration
substanceIndexOne record per substance (~580 stored): summary, dosage, duration, pharmacology, harm potential, legality, citations, and references.
subjectiveEffectsEffect articles such as geometry and euphoria, with VCode bodies, galleries, and audio replications (~230 stored).
tripReports / tripReportSubstancesTrip reports (~165 stored) linked to substances and doses. tripReportSubstances indexes report/substance-name pairs so article pages can find related reports without scanning the table.
replications / effectIndexArticlesReplication gallery metadata for videos, images, and audio, plus Effect Index methodology articles. R2 holds the media files.
categoryLayoutThe curated psychoactive layout read by /substances. Saving indexLayouts updates this table; the public page does not read indexLayouts directly.
indexLayouts / siteConfigHand-curated classification trees (psychoactive / chemical / mechanism) and about-page configuration.
moleculeOverridesCanonical substance and chemical-class drawings from the Molecules editor: editable MOL data, rendered SVG, and seeded, hand-drawn, or template-aligned provenance. Articles display the SVG; without a record, they show no drawing.
warningBannerPresetsSafety-banner text and its explicit substance-slug or sitewide scope. Editors can change the wording without redeploying.
copyBlocksEditable site text, one record per block, including this page. Copy Studio saves changes without a deploy; missing keys fall back to the checked-in JSON seed.
reagentTestsPoint-in-time ProtestKit reagent-test results, keyed by substance slug.
substanceGalleriesPer-substance Replication Showcase curation: a pinned first item and suppressed replications slugs.
replicationPlaylistsNamed, ordered replication sets applied to gallery drafts. Saving a playlist does not publish it.
Editorial records
tripReportSubmissionsPrivate /reports/submit intake. Editors review submissions before promoting them into public tripReports records.
membershipsEmail-linked membership and role records, read by next-auth at sign-in.
changelogEditor-save history: author, time, and Markdown diff. Viewable at /dev/change-log.
contributorProfilesPublic bios, aliases, links, and avatars for contributors.
articleFeedbackPrivate issues and suggested edits submitted from article feedback boxes. Visible only to authorised editorial users, never published.
siteFeedbackPrivate /about/feedback submissions. Visible only to authorised editorial users, never published.
mailingListSubscribersPrivate signups shared by the portal's four public sites, submitted through each site's own /api/subscribe route.
moleculeClassTemplatesEditor-authored scaffold-orientation templates for the Molecules editor. These editor-only cosmetic settings do not change published molecules.
Pipeline data
articleSourcesRaw per-substance text from the source sites, used for AI quote extraction.
prompts / quotesEditable section-generation prompts and verbatim quotes extracted from sources.
citationEvidenceCitation-audit records linking claims to reference IDs, supporting quotes, and review status.
replication* provenance + identityAbout 17 replication* and contributor* tables retain source attribution, artist/contributor identity bindings, alias and taxonomy evidence, duplicate reconciliation, date research, and reversible merge/social operations. Separate display records keep gallery reads small.
effectIndexArchiveLossless Effect Index source records retained for reuse by Effect Index, outside dose.wiki's public read paths.
generatedPublicationOperationsSuccessful reviewed section publications. Proposal IDs prevent the same publication from being applied twice.

07 Four kinds of traffic, one database

Public site · read path

Anyone can read without signing in. Most pages are static, open to crawlers, and include search metadata.

Server components use route loaders in lib/next/ and cached lib/data/publicData.* readers. Cache tags identify related data for invalidation.

Browsers never connect to the database directly. Shared caches avoid a query on every visit, though generating or refreshing a page can read the database. Flow A follows that path.

Public writes · reader submissions

Trip reports, article feedback, site feedback, and mailing-list signups require no sign-in.

Each form POSTs to its own rate-limited API handler with a honeypot, payload cap, and hashed IP. Site feedback also requires Turnstile. Records enter private tables.

Editors can publish reviewed trip reports through Flow C. Feedback and mailing-list signups remain private.

/dev editor · write path

Authorised users sign in on the editor host. Membership roles are admin, editor, translator, contributor, and viewer; viewer has no editor access, and the other roles have distinct permissions. src/middleware.ts enforces the page and host gates.

Whole-article saves use save-orchestrator/ and /api/save-article; other tools use /api/dev/*. Routes enforce session roles and named rate limits. The browser never holds an admin key.

Source-backed generation runs in authenticated local batches. The browser editor supports manual review and editing without exposing scraped source text.

Machine reads · open data + API

Researchers, mirrors, and other applications can read the data without signing in.

/open-data/SubstanceIndex.json, /open-data/EffectIndex.json, and /open-data/TripReports.json provide daily snapshots with per-dataset licensing metadata. /api/v1 provides versioned JSON endpoints described by /api/v1/openapi.json.

Both interfaces are read-only and cache-friendly. Pages still draw their content from the database, not the export files.

Licensing varies by dataset. Base data is CC0, but exports include exceptions: TripSit's terms on substance interactions, third-party replication media in the effect index, and authors' rights on legacy trip reports. See License & reuse.

08 Scripts and pipelines: the third system

Node and Bun scripts parse scraped sources (scripts/parsers/), prepopulate high-confidence fields (scripts/prepopulate/), and draft prose through OpenRouter (scripts/batch/). Citation and legality workflows research and check supporting sources; claims can remain uncited. Editors review the results in /dev. Article text and citations have separate, ongoing review workflows, and each article displays its review status.

Local execution does not imply a local database. Check the target before any write. Production commands require DATA_BACKEND=postgres, an explicit --target or TARGET_POSTGRES_URL, an allowlisted operation, and every command-specific guard. Shared wrappers default to a dry run and require --write, --confirm-write=<operation>, and a matching --expected-deployment where supported. DATA_WRITES_FROZEN=1 blocks writes. Neither credentials nor a successful dry run authorise publication.

scripts/lib/data-ops-run-context.mjs resolves intent-scoped tokens (DATA_ADMIN_TOKEN_<INTENT>); scripts/lib/production-write-command.mjs supplies shared write gates. Migration-style scripts use pre-write backups and scripts/data/audit-logs/. Follow docs/operations/data-credentials.md for native environment diagnostics, the current allowlist, and the exact write procedure.

Workflow families have their own subdirectories and package commands:

  • scripts/citations/verdict-*: citation-support audits, run by the citation workflow in docs/workflows/citations.md.
  • scripts/legality/: legal-source research, run by the legality workflow in docs/workflows/legality.md.
  • scripts/replications/: media intake and R2 migration, the largest family.
  • scripts/contributors/: contributor avatars.
  • scripts/chemistry/: molecule and chemistry audits.
  • scripts/migrate/, scripts/data-ops/: guarded Postgres migration and export tooling.
  • scripts/build/: social-card and theme-CSS generators.

scripts/lib/workflow-command-surface.mjs and its sibling citation-command-surface.mjs index runnable commands; package.json defines the npm run entry points.

How substance articles are made explains scraping, verbatim quote extraction, section prompts, citation proposal and auditing, and human review. It publishes unedited extraction and section-generation prompts, but not citation or legality workflow prompts. Published prompts may differ from those used for a particular article.

09 The replications media platform

Replications are artist-made videos, images, and audio recreations of subjective effects. Postgres stores metadata; Cloudflare R2 stores files under content-addressed keys derived from their contents. server/lib/replicationUrls.ts constructs public URLs using REPLICATION_MEDIA_BASE_URL, required in every deployment.

scripts/replications/ uploads and verifies reviewed media before importing metadata, taxonomy, and attribution into Postgres. The Reddit archive follows docs/workflows/replication-intake.md. In Replication Studio, editors curate carousels, substance showcases, and playlists, and reconcile duplicates. Separate provenance and identity records preserve the source history.

substanceGalleries controls pinned and suppressed showcase items; replicationPlaylists supplies ordered sets to gallery drafts; siteConfig stores the Effect Index homepage carousel order. These database edits need no deploy. Public reads use lib/data/publicData.replications.ts, /api/replications/gallery, and /api/replications/showcase; both JSON endpoints are cacheable.

Known artists are credited; unidentified creators are shown as Unattributed. Rights default to unknown, independently of artist attribution. Source posts and posters are retained losslessly. The gallery's fair-use position appears in src/features/replications/replicationsFairUseCopy.ts and License & reuse.

10 Three flows, step by step

Flow A: a reader opens dose.wiki/lsd

Readers usually receive a cached page. When the page is generated or refreshed, this sequence runs:

  1. Route

    The server component in src/app/[slug]/page.tsx runs.

  2. Fetch

    A lib/next/ loader calls the tagged, cached, read-only lib/data/publicData.* readers.

  3. Validate

    lib/data/publicData.substanceContract.ts validates the record against the Zod schemas in src/schema/.

  4. Render

    src/features/article/ renders sections, tables, and citations.

  5. Serve

    The page includes JSON-LD search metadata and a substance-specific social card. Incremental static regeneration allows refresh after an hour; editor saves also invalidate affected paths and cache tags.

Flow B: an editor saves an article in /dev

  1. Edit

    React Hook Form holds edits in src/features/dev/forms/.

  2. Collect changes

    save-orchestrator/ collects articles with unsaved changes.

  3. Check access and input

    /api/save-article checks the session role, rate limit, and Zod contract.

  4. Save

    substanceIndex.saveSubstances writes the changes and creates a changelog entry.

  5. Refresh

    Publication invalidates local caches and requests revalidation on public deployments. A toast reports the save result.

Flow C: a reader submits a trip report

  1. Write

    /reports/submit collects the report using src/features/reports/submissions/.

  2. Check the submission

    /api/trip-report-submissions enforces a rate limit, honeypot, and 128 KB cap. The submitter's IP is stored only as a salted hash.

  3. Store privately

    The report enters tripReportSubmissions, not the public catalog.

  4. Review

    The /dev Trip Report Portal shows the queued report with a needs-review badge.

  5. Approve for publication

    An editor previews the result, assigns authorship, and confirms through /api/trip-report-submissions/[id]/promote.

  6. Publish

    A tripReports record makes the report available at /reports/[slug].

11 Recurring vocabulary

Substance article
A structured substance record with about 15 sections, defined by src/schema/ and stored in substanceIndex.
VCode
Custom markup for effect articles, stored with a parsed syntax tree (AST) and rendered by src/features/effects/.
Database
The shared Postgres store for content, editorial records, and pipeline data. A database record is not necessarily public.
Site flavor
The build-time choice of dose.wiki or Effect Index, distinct from the public/editor deployment boundary.
Copy block
A keyed piece of editable site text. getCopy() reads it from copyBlocks, falling back to the checked-in JSON when absent.

12 Where to start reading

For a first pass through the code:

  1. README.md, ARCHITECTURE.md: orientation, routes, package commands, and system layout.
  2. server/schema.ts: the data model; start with substanceIndex.
  3. src/app/[slug]/page.tsxlib/next/routeLoaders.substances.tsxlib/data/publicData.*src/features/article/: follow the public read path in Flow A.
  4. src/middleware.ts, auth.ts: host routing, sign-in, and roles. Verify deployment routing with a production-shaped build, not just next dev.
  5. src/lib/http/protectedRouteOperation.ts, lib/http/rateLimitPolicy.ts: protected API handling, session roles, named rate limits, and payload caps. Public intake uses separate controls.
  6. src/features/dev/save-orchestrator/, src/app/api/save-article/: follow Flow B's saves and audit trail.
  7. scripts/lib/workflow-command-surface.mjs: find batch, citation, replication, migration, and sync entry points.
Caveats about this page

Code and stored-record counts are approximate inventory snapshots; check the current tree and database. This overview simplifies rate limits and host routing. Consult AGENTS.md, docs/glossary.md, and docs/architecture/ before operational changes.