Μετάβαση στο περιεχόμενο
ErmisAI

Local development setup

Getting ErmisAI running locally: prerequisites, environment, database, and the first run.

This page gets a working ErmisAI dev server on your machine and explains the version pins and environment traps that break a first run. Paths are relative to the app repository root (the directory is named hermesai; the package is ermisai).

Prerequisites

RequirementVersionWhere it is pinned
Node.js>=24.18.0package.json:7 (engines.node)
pnpm11.15.1package.json:5 (packageManager)
Postgresany standard Postgresoptional for a first boot — see Running without Postgres
Clerk applicationpublishable key + secret keyrequired; the app ships no auth form of its own

pnpm is the only supported package manager. Corepack pins it to 11.15.1 from the packageManager field regardless of what you have installed globally.

mise.toml contains exactly one entry:

[tools]
pnpm = "latest"

It does not pin Node, and there is no .nvmrc or .tool-versions in the repo. Your Node version is your responsibility. CI runs Node 24 (.github/workflows/ci.yml). Node 20 will refuse to install.

Install

pnpm install

Environment file

Copy the committed example and fill it in:

cp .env.example .env

.gitignore ignores .env* and un-ignores only .env.example, so nothing you write into .env or .env.local can be committed by accident.

.env.example (242 lines) is the most complete environment reference in the repository — more complete than README.md. The README contains no mention of MIGRATIONS_DATABASE_URL, POSTGRES_POOL_MAX, ERMIS_QUEUE_SIGNING_SECRET, FLAGS_SECRET, EMAIL_UNSUBSCRIBE_SECRET, SENTRY_DSN, APP_URL, or the ERMIS_FEED_* tuning block. Read .env.example first, and see Environment variable reference for the full grouped list.

The minimum to boot

You do not need Polar, Resend, Upstash, Sentry or an AI provider to get the dev server up. The genuine minimum is Clerk plus one of the two storage choices below.

Ιδιότητα

Τύπος

README.md:34-37 also asks you to set the four Clerk URL variables in both their server and NEXT_PUBLIC_ forms: CLERK_SIGN_IN_URL=/sign-in, CLERK_SIGN_UP_URL=/sign-up, and both fallback redirect URLs to /app.

Running without Postgres

ERMIS_FORCE_INMEMORY_STORE=true

This routes the shared runtime state store (platform_state / platform_state_hash) to an in-memory implementation, so the app boots with no database at all. Everything you create is lost when the dev server restarts, and nothing that pnpm db:seed-bootstrap would have written — the billing plan rows, the synced source catalog rows — exists.

isInMemoryStoreForced() returns false whenever VERCEL_ENV is set, whatever the flag says (src/lib/platform/shared/persistence.ts:16-22). The presence of VERCEL_ENV is a hard gate that wins over the flag, on preview deployments as well as production, so a stray value copied into a Vercel environment can never silently route tenant state to memory. It is a local switch only.

Production has no state fallback at all: if platform_state is unreachable or unmigrated, every authenticated surface fails. That is deliberate. See Database schema and runtime state.

Database setup

Two URLs, two resolution orders

The application runtime and drizzle-kit deliberately read different variables in a different order.

ConsumerResolution orderSource
App runtimeSUPABASE_DATABASE_URLDATABASE_URLsrc/lib/db/client.ts:66-69
drizzle-kit (all db:* scripts)MIGRATIONS_DATABASE_URLSUPABASE_DATABASE_URLDATABASE_URLdrizzle.config.ts:10-33

MIGRATIONS_DATABASE_URL exists because DDL through a transaction-mode pooler is unreliable. Point it at the direct or session endpoint. On CapyDB the same host serves both: :6432 is the pooled endpoint for the app runtime, :5432 is the direct endpoint for DDL only (.env.example:19-25).

In NODE_ENV=production a direct host in the runtime variable throws at client construction rather than falling through to another candidate (src/lib/db/client.ts:97-104):

<NAME> points at a direct Postgres host. Use the pooled/transaction connection string for the app runtime in production.

Locally this guard does not fire, so a single direct URL in DATABASE_URL is fine for development — but it will fail closed the moment the same value reaches production.

Migrate and seed

Apply migrations

pnpm db:migrate

drizzle-kit resolves the connection string from the repo's env file itself, so this works without exporting anything into your shell.

Seed the bootstrap data

pnpm db:seed-bootstrap

Upserts the six billing plans, the whole source catalog, and a dataset-version row in one transaction. On success it prints:

Bootstrap seed completed.
billing_plans=<n>
source_catalog_entries=<n>
source_catalog_dataset_versions=<n>
catalog_entries=<n>
catalog_version=<v>

Without it, billing_plans is empty, so the Billing and Usage sections of Settings and GET /api/billing/plans have no plan rows (src/lib/db/billing-repository.ts:465). source_catalog_entries being empty is quieter: the repository falls back to the bundled news-sources/catalog.v1.json when the table returns zero rows (src/lib/db/source-catalog-repository.ts:613-614, :472-487), so the source picker still renders the full catalog — a failed or skipped sync looks identical to a healthy deployment.

Confirm the schema matches

pnpm db:check

Runs drizzle-kit check --dialect postgresql. It also runs in the CI quality job.

pnpm db:seed-bootstrap runs through tsx, and tsx does not load .env — the script reads process.env only (scripts/db/seed-bootstrap.ts:107-128). If your connection string lives only in the file, either export it for the command or invoke the script the way email:campaign does in package.json:26:

node --env-file=.env --import tsx scripts/db/seed-bootstrap.ts

Two one-off repair scripts sidestep this by calling process.loadEnvFile('.env') themselves when no DB URL is present: scripts/db/backfill-source-icons.ts and scripts/db/repair-source-catalog-categories.ts. Nothing else does.

Never use npx for drizzle-kit

Use pnpm db:migrate or pnpm exec drizzle-kit. The production migration gate spells out why in its own header: it spawns pnpm exec and "NOT npx — npx re-resolves and trips the version gate" (scripts/deploy/prod-migrate.ts:14-15).

The repo is on drizzle-kit and drizzle-orm 1.0.0-rc.4, whose layout is one directory per migration (drizzle/<timestamp>_<name>/migration.sql + snapshot.json) with intentionally no drizzle/meta/_journal.json. If you are about to change schema.ts, read Migrations and schema changes first.

First run

pnpm dev

Open http://localhost:3000.

To confirm the backend is actually wired up, hit the readiness probe — it is one of only two API routes that require no authentication:

curl -s http://localhost:3000/api/health | jq

It returns HTTP 200 when checks.database.status === 'ok' and 503 otherwise (src/app/api/health/route.ts:144,161). Redis being absent reports "status": "not_configured" and never fails readiness. The useful field on day one is checks.database.migrated: when the platform_state probe fails you get migrated: false and

platform_state unreachable (run db:migrate before serving traffic): <error>

The config block lists ten launch-critical environment keys and whether each is present. It is informational locally — config.ready: false does not change the HTTP status. See Deploying to production.

Version pins that must not change

Never remove @typescript/native-preview from devDependencies while the repo is on typescript@7. typescript@7.0.2 ships only lib/tsc.js, lib/getExePath.js and lib/version.cjs — there is no lib/typescript.js, i.e. no programmatic compiler API. Next.js needs one of two escape hatches, and this repo sets both: experimental.useTypeScriptCli: true (next.config.ts:304) and the native-preview package. Knip reports it as an unused dependency, which is why it is listed in ignoreDependencies in knip.json — do not "clean it up".

Bleeding-edge dist-tags (canary, next, rc, beta, experimental) are deliberate policy across the dependency tree. Never downgrade one to fix a local problem.

The cost of that policy is drift: an option that a next canary drops becomes a type error on your machine with no change from you. This is real and currently visible — pnpm typecheck fails on next.config.ts:435:

next.config.ts(435,5): error TS2353: Object literal may only specify known properties,
and 'imgOptSkipMetadata' does not exist in type 'ExperimentalConfig'.

When pnpm typecheck goes red in a file you never touched, check next.config.ts against the installed canary before assuming your branch is at fault.

Day-one surprises

  • pnpm lint prints nothing. It is oxlint -f github src/ > lint.md 2>&1 (package.json:32) — both streams are redirected into lint.md at the repo root. Read that file, or the exit code. Use pnpm lint:fix for terminal output.
  • pnpm build performs no type checking at all. typescript.ignoreBuildErrors: true is set deliberately (next.config.ts:113-118) because the build-time check needs the programmatic TS API that native TS 7.0 does not expose. Type safety comes solely from pnpm typecheck.
  • pnpm build hard-sets NODE_ENV=production (package.json:11-12). A NODE_ENV=development exported in your shell otherwise leaks into next build and breaks prerendering.
  • pnpm lint and pnpm format only cover src/. scripts/, tests/ and drizzle/ are outside their scope.
  • pnpm test is noisy on a green run. Suites deliberately exercise failure paths, so lines like [news-pipeline] synthesis attempt failed (…) and [billing-status] unrecognized Polar subscription status … are expected output, not failures.
  • AGENTS.md rewrites itself. next dev regenerates it (node_modules/next/dist/server/lib/generate-agent-files.js). Removing it from a diff just recreates the uncommitted change.
  • Formatting is oxfmt, linting is oxlint. There is no ESLint config and no Prettier config in the repo. .oxfmtrc.jsonc sets no semicolons, single quotes and printWidth 100; .oxlintrc.json sets "typeAware": true, so even a plain pnpm lint runs type-aware rules.

Commands you will use most

CommandWhat it does
pnpm devDev server on http://localhost:3000
pnpm typechecktsc --noEmit with the native TS 7 compiler; prints an extended diagnostics block
pnpm testvitest run over tests/**/*.vitest.ts
pnpm vitest run tests/<name>.vitest.tsOne suite
pnpm lint / pnpm lint:fixoxlint into lint.md / oxlint with terminal output and autofix
pnpm formatoxfmt over src/
pnpm buildProduction build, no type checking
pnpm db:generate / db:migrate / db:studiodrizzle-kit

Tests live in tests/*.vitest.ts and are not colocated with source. They run in plain node with server-only aliased to an inert stub and part of the environment neutralised by tests/setup.env.ts. The full script inventory and the structural gates that fail CI are on Scripts, quality gates and CI.

Σε αυτή τη σελίδα