Better Auth integration
Current state
Harvy uses Better Auth 1.7.13 in a separate TypeScript Worker (v2/auth/) with D1. The Python app Worker remains the scraper/API host and forwards /api/better-auth/* to the auth service through HARVY_AUTH.
The integration is staged and disabled by default: BETTER_AUTH_ENABLED is false. The existing /api/auth/* custom login remains active while the flag is false; at cutover those routes return 410 and legacy sessions/keys are checked as a compatibility fallback. Do not enable the new route or apply migrations remotely as part of a normal deploy.
Auth source is grouped under v2/auth/: the Better Auth client/UI are client.ts and ui.ts, and the legacy-compatible modal controller is public/auth-modal.js. Vite bundles client.ts to static/auth-client.js and copies the modal controller to static/auth-modal.js, preserving the existing page URLs. Auth styles live in styles/stylesheet.css; static/stylesheet.css is generated. The static files are build outputs, not independent source files.
Configured plugins
- Core: email/password with email verification/reset (enabled only when Infrastructure and email feature flags are true), Discord/GitHub/Google/Twitter OAuth (each provider appears only when its client ID and secret exist).
- Local plugins: anonymous accounts, passkeys, authenticator-app TOTP, admin, API keys (
harvy_live_prefix), organizations, Turnstile CAPTCHA, last-login method, multi-session, and referrals. - Better Auth Infrastructure: dashboard/audit logging, Sentinel, and transactional email require
BETTER_AUTH_API_KEYplusBETTER_AUTH_INFRA_ENABLED=true; email/password additionally requiresBETTER_AUTH_EMAIL_ENABLED=true. This is a separate service/plan and data is sent to Better Auth Infrastructure when active. - Stripe: the plugin is conditionally loaded only when
STRIPE_ENABLED=trueand the Stripe secret/webhook are configured. It creates no subscription checkout until plans/prices are explicitly configured.
The Harvy Buttondown newsletter key and Buttondown subscription flow are separate from Better Auth transactional email. Never reuse the Buttondown key as BETTER_AUTH_API_KEY.
Required secrets/configuration
Use Cloudflare Worker secrets (do not commit values or paste them into documentation):
npx wrangler secret put BETTER_AUTH_SECRET --config v2/auth/wrangler.jsonc
npx wrangler secret put BETTER_AUTH_API_KEY --config v2/auth/wrangler.jsonc
npx wrangler secret put DISCORD_CLIENT_ID --config v2/auth/wrangler.jsonc
npx wrangler secret put DISCORD_CLIENT_SECRET --config v2/auth/wrangler.jsonc
npx wrangler secret put GITHUB_CLIENT_ID --config v2/auth/wrangler.jsonc
npx wrangler secret put GITHUB_CLIENT_SECRET --config v2/auth/wrangler.jsonc
npx wrangler secret put GOOGLE_CLIENT_ID --config v2/auth/wrangler.jsonc
npx wrangler secret put GOOGLE_CLIENT_SECRET --config v2/auth/wrangler.jsonc
npx wrangler secret put TWITTER_CLIENT_ID --config v2/auth/wrangler.jsonc
npx wrangler secret put TWITTER_CLIENT_SECRET --config v2/auth/wrangler.jsonc
npx wrangler secret put TURNSTILE_SECRET_KEY --config v2/auth/wrangler.jsonc
npx wrangler secret put STRIPE_SECRET_KEY --config v2/auth/wrangler.jsonc
npx wrangler secret put STRIPE_WEBHOOK_SECRET --config v2/auth/wrangler.jsonc
npx wrangler secret put HARVY_ADMIN_USER_IDS --config v2/auth/wrangler.jsoncTURNSTILE_SITE_KEY is public configuration; set it as a Worker variable after creating the Turnstile widget. Set BETTER_AUTH_INFRA_ENABLED, BETTER_AUTH_EMAIL_ENABLED, and STRIPE_ENABLED to true only when each service is ready. OAuth callback URLs use https://harvybot.com/api/better-auth/callback/{discord|github|google|twitter}. Set BETTER_AUTH_ENABLED=true on the main Worker only after all providers, migrations, privacy checks, and the UI have been tested.
D1 and legacy accounts
Better Auth tables are generated in v2/auth/auth-schema.ts; the reviewed D1 migration is migrations/0005_better_auth.sql, followed by the privacy-consent audit table in migrations/0006_auth_privacy_consents.sql. The migrations have not been applied to production. Back up D1, review, and test locally before applying remotely.
The old harvy_users, harvy_passkeys, harvy_sessions, and harvy_api_keys tables remain intact. Existing harvy_live_ keys are checked during the compatibility window. Legacy passkey records are not imported: users will re-enroll a Better Auth passkey rather than risk accepting incompatible credential data. Do not remove legacy tables until migration and account recovery are verified.
After changing server plugins, regenerate v2/auth/auth-schema.ts with the pinned Better Auth CLI version, generate and review a Drizzle SQLite migration, then place the reviewed SQL in the next root migrations/ sequence. Use an ephemeral schema-generation secret; never put actual secrets in the schema config or migration. Test all migrations together with the local SQLite migration test before requesting a remote migration.
Tenant/data boundary
Organizations represent customer environments, not ownership of shared ontology objects. Shared Harvy objects remain globally shared; observations, links, annotations, dashboards, and other customer-private records must be scoped to the active environment and authorized server-side. Never cache private results across environments.