The Deni AI help center (support.deniai.app), built with Fumadocs on TanStack Start.
pnpm install
pnpm devpnpm run types:check
pnpm run lint
pnpm run buildArticles live in content/docs/. Each article has an English file and a
Japanese file next to it:
content/docs/flixa/external-tools.mdx -> /en/flixa/external-tools
content/docs/flixa/external-tools.ja.mdx -> /ja/flixa/external-tools
Sidebar order and folder titles come from meta.json (use meta.ja.json for a
Japanese title that differs). / redirects to /en or /ja based on
Accept-Language.
Every page has a "Get help" widget (bottom right) where an AI assistant
(Claude Haiku 5.5) answers questions by searching and reading the help center
articles. The contact and feedback forms, which email the team, also live in
the widget: its "Contact support" and "Send feedback" buttons open them, and the
assistant can draft either message as a card in the chat that the user reviews,
edits, and sends. Messages sent from the widget carry the conversation.
Links to /en/contact and /en/feedback (and the /ja/ versions, with an
optional ?category= on contact) open the forms in the widget; visiting those
URLs redirects to the help center home with the form open. Each article also
ends with a "Was this page helpful?" prompt.
With a database configured, widget conversations are saved so the team can answer contact and feedback messages:
- Each chat exchange and each message sent to the team is stored in the
supportschema. The browser keeps the conversation's credential (<id>.<HMAC token>) inlocalStorageand restores the chat on the next visit. - The team works from the admin inbox at
/admin(password:SUPPORT_ADMIN_TOKEN), which lists conversations with a contact or feedback message. Notification emails to the team link to the conversation there. - A reply appears in the visitor's chat (with an unread dot on the "Get help" button) and is emailed to the address they gave, with a link that opens the conversation in the widget on any device. The visitor answers with the reply's "Reply" button, which reopens the conversation in the inbox.
- "New chat" starts a new conversation; earlier ones stay reachable from the reply emails.
They are configured with runtime environment variables, listed in
.env.example. For local development, copy it to .env.local. All are
optional: without ANTHROPIC_API_KEY the widget shows only the contact and
feedback buttons; without the Cloudflare email credentials the forms say they are
unavailable; without both Turnstile keys nothing is challenged; without
DATABASE_URL and SUPPORT_SECRET nothing is saved and replies go by email
only. Requests are also rate limited per IP in memory.
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY |
AI support chat |
CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN |
Contact and feedback email (Email Sending: Edit) |
SUPPORT_EMAIL_TO, SUPPORT_EMAIL_FROM |
Recipient and sender (default contact@deniai.app, Deni AI Support <noreply@deniai.app>) |
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY |
Cloudflare Turnstile for the chat and forms |
DATABASE_URL |
Postgres (the Deni AI database) for saved conversations; the pooled URL works |
SUPPORT_SECRET |
Long random string that signs conversation links and admin sessions; changing it detaches every saved conversation from its browser |
SUPPORT_ADMIN_TOKEN |
Password for the admin inbox at /admin |
The tables live in the support schema of the Deni AI database
(src/server/db/schema.ts); deni-ai and Flixa leave that schema alone. After
changing the schema, generate a migration and review the SQL in migrations/:
pnpm run db:generateApply migrations to the database in DATABASE_URL from .env (production) or
.env.local (development) before deploying code that needs them:
pnpm run db:migratepnpm run db:migrate:devProduction runs on Dokploy from the Dockerfile (Build Type: Dockerfile,
port 3000, domain support.deniai.app). Set the support variables above as
runtime environment variables; no build arguments are needed. Run
pnpm run db:migrate against the production database before the first deploy
with DATABASE_URL.
pnpm run build produces a Node server in .output/. It still imports a few
packages at runtime, so start it next to node_modules:
pnpm startThe server listens on PORT (default 3000).