Dochat - AI Customer Support Agent SAAS
- Framework: Next.js 15 (Turbopack)
- Monorepo: pnpm workspaces + Turborepo
- Database: PostgreSQL + Prisma
- Auth: Clerk
- Payments: DodoPayments
- AI: DigitalOcean GenAI Platform
- Storage: DigitalOcean Spaces (S3-compatible)
- Real-time: Server-Sent Events (SSE) + in-memory event bus
apps/
web/ → Main dashboard app (Next.js)
widget/ → Embeddable chat widget (Next.js)
packages/
db/ → Prisma schema & client
ui/ → Shared UI components
shared/ → Shared utilities
apps/web/lib/
digitalocean/ → DO GenAI API integration layer
types.ts → Shared API request/response types
errors.ts → DoApiError class
client.ts → Typed HTTP client (doFetch, doFetchRaw)
agents.api.ts → Agent CRUD, access keys, visibility
knowledge-bases.api.ts → KB CRUD, datasource management
workspaces.api.ts → Workspace CRUD
indexing.api.ts → KB indexing triggers & status polling
agent.ts → Agent provisioning, sync, chat orchestration
knowledge-base.ts → KB provisioning, indexing, datasource building
auth.ts → Clerk auth helpers, subscription bootstrapping
event-bus.ts → In-memory pub/sub for real-time SSE events
limits.ts → Plan-based usage limits & credit checks
The DigitalOcean GenAI integration follows a layered architecture:
- Types & Errors (
types.ts,errors.ts) — Shared type definitions and error classes - HTTP Client (
client.ts) — Authenticated fetch wrapper with error handling - API Modules (
*.api.ts) — Low-level DO API calls, one module per resource - Service Layer (
agent.ts,knowledge-base.ts) — Business logic that orchestrates API calls with database operations
pnpm install
cp .env.example .env # fill in your values
pnpm db:generate
pnpm db:push
pnpm devWeb app: http://localhost:3005 | Widget: http://localhost:3006
Create a .env file in the project root:
# Database
DATABASE_URL=
# Clerk Auth
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
# DigitalOcean GenAI
DIGITALOCEAN_API_TOKEN=
DO_PROJECT_ID=
DO_AGENT_REGION=
DO_AGENT_MODEL_UUID=
DO_EMBEDDING_MODEL_UUID=
DO_FREE_OPENSEARCH_DB_ID=
# Widget
NEXT_PUBLIC_WIDGET_URL=http://localhost:3006
# Landing page widget (optional — shows demo widget on marketing page)
NEXT_PUBLIC_LANDING_WIDGET_ORG_ID=
NEXT_PUBLIC_LANDING_WIDGET_AGENT_ID=
# DigitalOcean Spaces (S3-compatible storage for KB file uploads)
SPACES_ACCESS_KEY_ID=
SPACES_SECRET_ACCESS_KEY=
SPACES_BUCKET=
SPACES_REGION=
# DodoPayments
DODO_PAYMENTS_API_KEY=
DODO_PAYMENTS_ENVIRONMENT=
DODO_PAYMENTS_WEBHOOK_SECRET=
DODO_STARTER_PRODUCT_ID=
DODO_GROWTH_PRODUCT_ID=
DODO_SCALE_PRODUCT_ID=- DigitalOcean account with GitHub connected (authorize repo access)
doctlCLI installed and authenticated:brew install doctl doctl auth init
- A PostgreSQL database (managed or external) with
DATABASE_URLin your.env
bash .do/deploy.shThat's it. The script reads your .env, injects secrets into the app spec, and creates/updates the app via doctl. No secrets are committed to git.
.do/app.yamldefines a two-service app spec (web + widget) with__PLACEHOLDER__values for secrets.do/deploy.shreplaces placeholders with real values from.envat deploy time- The generated spec is temporary and deleted after deployment
deploy_on_push: trueauto-deploys on every push tomain- Ingress routes
/widget/*to the widget service, everything else to the web service - DO App Platform strips the
/widgetprefix before forwarding — the widget usesassetPrefix(notbasePath) accordingly
After the first successful deployment, push your database schema:
DATABASE_URL="<your-db-url>" pnpm db:pushAdd via the dashboard under App > Settings > Domains, or in .do/app.yaml:
domains:
- domain: yourdomain.com
type: PRIMARYThen CNAME your domain to the .ondigitalocean.app URL.