Docs
Self-hosting
Deploy your own copy of anymd on Cloudflare Workers with D1, KV, R2, Vectorize and Workers AI, plus secrets and CI.
anymd is MIT-licensed and runs as a single Cloudflare Worker. Everything the hosted service uses is in the repository at github.com/digitopvn/anymd.
What you need
- A Cloudflare account with Workers, D1, KV, R2, Vectorize and Workers AI available.
- Node.js and npm.
- A domain on Cloudflare, if you want a custom hostname.
1. Clone and install
git clone https://github.com/digitopvn/anymd.git
cd anymd
npm install
npx wrangler login
2. Create the Cloudflare resources
The repository's wrangler.jsonc declares two environments, staging and production. Each needs:
| Binding | Type | Used for |
|---|---|---|
DB |
D1 database | Users, keys, library, usage, traces, billing, pages, posts |
OAUTH_KV |
KV namespace | MCP OAuth state |
CACHE |
KV namespace | Conversion cache, anonymous counters, published-page cache |
MEDIA |
R2 bucket | Media, social images, CLI tarball |
VECTORS |
Vectorize index | Semantic search embeddings |
AI |
Workers AI | Embeddings, query fan-out, document conversion |
RL_ANON, RL_AUTH |
Rate limiting | Per-minute limits for anonymous and signed-in callers |
RL_DOMAIN |
Rate limiting | Per-minute fetch budget per target site, shared by all callers |
Create them (production shown; repeat with -staging names for staging):
npx wrangler d1 create anymd-production
npx wrangler kv namespace create OAUTH_KV
npx wrangler kv namespace create CACHE
npx wrangler r2 bucket create anymd
npx wrangler vectorize create anymd-chunks-production --dimensions=1024 --metric=cosine
Vectorize metadata indexes
Semantic search filters vectors by owner, and deletes them by document. Create both metadata indexes before any document is embedded, because Vectorize only indexes metadata on vectors written after the index exists:
npx wrangler vectorize create-metadata-index anymd-chunks-production --propertyName=user_id --type=string
npx wrangler vectorize create-metadata-index anymd-chunks-production --propertyName=doc_id --type=string
The index dimension (1024) matches the bge-m3 embedding model used by the library.
3. Point wrangler.jsonc at your resources
wrangler.jsonc ships with Digitop's account and resource ids. Replace, per environment:
account_idd1_databases[].database_id(anddatabase_nameif you renamed it)kv_namespaces[].idforOAUTH_KVandCACHEr2_buckets[].bucket_nameandvectorize[].index_nameif you renamed themrouteswith your own hostnamesratelimits[].namespace_id: any integer unique within your accountvars:PUBLIC_URL,CDN_URL,GITHUB_REPO,POLAR_SERVER(sandboxorproduction)
Serve the R2 bucket from a custom domain (Cloudflare dashboard → R2 → your bucket → Custom domains) and put that origin in CDN_URL.
The SSRF guard in src/convert/index.ts refuses to convert anymd's own hostnames. Add your hostnames to its blocked list so your deployment can't be asked to fetch itself.
4. Apply database migrations
Migrations live in migrations/. Apply them to the remote database:
npm run db:migrate:staging
npm run db:migrate:production
These scripts use the database names anymd-staging and anymd-production; adjust package.json if yours differ. For local development, apply them to the local database instead:
npx wrangler d1 migrations apply anymd-staging --local --env staging
5. Set secrets
First, set ADMIN_EMAILS in wrangler.jsonc (a plain var, per environment). It holds the comma-separated emails that become owner when they sign up.
All secrets are optional. Features that need one stay off, or degrade gracefully, until it is set. Set them per environment, one by one or from your .env in one go:
npx wrangler secret put OPENROUTER_API_KEY --env production
npm run secrets:production # pushes every known secret found in .env, without printing values
| Secret | Enables |
|---|---|
POLAR_ACCESS_TOKEN |
Polar checkout, customer portal and usage-based overage |
POLAR_WEBHOOK_SECRET |
Verifying Polar webhooks |
GITHUB_TOKEN |
Higher GitHub API rate limit for the changelog |
RESEND_API_KEY |
Transactional email (welcome, password reset) |
RAPIDAPI_KEY, VIDCAP_API_KEY |
YouTube transcripts (either works; RapidAPI is tried first) |
OPENROUTER_API_KEY |
Query fan-out and Jev via OpenRouter (Workers AI is the fan-out fallback) |
TYPESAFE_API_KEY |
Jev directly from TypeSafe, when OpenRouter isn't configured |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
"Continue with GitHub" sign-in |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
"Continue with Google" sign-in |
Sign in with GitHub or Google
A provider's button appears only when both its client id and secret are set. Register this callback URL with the provider, using your own host:
https://anymd.cc/api/auth/oauth/github/callback
https://anymd.cc/api/auth/oauth/google/callback
The callback defaults to PUBLIC_URL plus that path; set GITHUB_CALLBACK_URL or GOOGLE_CALLBACK_URL only to override it. A GitHub OAuth App accepts one callback host, so staging needs its own app: put its values in .env as STAGING_GITHUB_CLIENT_ID and STAGING_GITHUB_CLIENT_SECRET, and npm run secrets:staging pushes them. A Google client can serve both hosts once the staging callback is added to its authorized redirect URIs. Request the user:email (GitHub) and openid email profile (Google) scopes; anymd asks for them itself.
Sign-in links to an existing account only through an email address the provider marks as verified. Because sign-up with a password does not verify email, linking removes any password on that account and signs out its sessions; the owner can add a password again with "Forgot password".
The authoritative list is the Env interface in src/env.ts. For local development, put values in .dev.vars (git-ignored). Never commit secrets.
Billing with Polar
Use POLAR_SERVER=sandbox for anything that isn't production. anymd finds your Polar products by metadata, so no product ids live in config: give each recurring product anymd_plan = pro or scale. Discount codes are plain Polar discounts with matching names (for example LAUNCH30). Without POLAR_ACCESS_TOKEN, billing stays off and everything else works.
scripts/polar-setup.mjs creates all of that in one run and is safe to re-run: the credits meter, the included-credit benefits, the four products, the discount codes and the webhook. It saves the webhook signing secret to .env without printing it:
npm run polar:setup -- --webhook https://staging.example.com/api/webhooks/polar # sandbox, POLAR_ACCESS_TOKEN
npm run polar:setup -- --production --webhook https://example.com/api/webhooks/polar # production, POLAR_PRODUCTION_ACCESS_TOKEN
npm run secrets:production then pushes POLAR_PRODUCTION_ACCESS_TOKEN and POLAR_PRODUCTION_WEBHOOK_SECRET as the production Worker's POLAR_ACCESS_TOKEN and POLAR_WEBHOOK_SECRET.
6. Build and deploy
npm run deploy:staging
npm run deploy:production
Each runs npm run build (Tailwind CSS and client scripts) and then wrangler deploy for that environment.
Local development runs against the staging configuration:
npm run dev
If Vectorize isn't reachable from local dev, hybrid search still returns keyword results.
7. Continuous deployment
The upstream repository deploys on push:
| Branch | Environment | Hostname |
|---|---|---|
dev |
staging |
staging.anymd.cc |
main |
production |
anymd.cc, www.anymd.cc |
The workflows live in .github/workflows/. For your fork, add two repository secrets, CLOUDFLARE_API_TOKEN (with Workers, D1, KV, R2 and Vectorize edit permissions) and CLOUDFLARE_ACCOUNT_ID, and change the hostnames. A deploy job boils down to:
npm ci
npm run typecheck
npm run db:migrate:production
npm run deploy:production
Apply migrations before deploying code that depends on them.
Operating it
- Logs:
npx wrangler tail --env production. Observability is enabled inwrangler.jsonc, so logs also appear in the Cloudflare dashboard. - Rollback:
npx wrangler rollback --env productionreturns to the previous Worker version. It does not undo D1 migrations. - Point clients at your copy: set
ANYMD_BASE_URLfor the CLI and use your hostname in MCP configs.
License
MIT. Copyright (c) 2026 Digitop.ai. Keep the license notice when you redistribute.
Updated 2026-09-26 · Edit on GitHub