Just A Good Guy Docs
Sign in

Developers

Self-hosting the central API

Run the whole platform yourself. Everything is optional except a public URL; missing services fall back to safe practice modes.

For developers
On this page

Running it

The API is one Node.js process with a SQLite database. It also serves the dashboard (/app), the widget (/w.js), these docs (/docs) and the landing page (/). It needs Node 24.

For development, from the repository:

Shell
npm install
cd apps/api
PUBLIC_URL=http://localhost:8100 npx tsx src/index.ts

For production, build everything from the repository root with npm run build, then run the API's built server.mjs with your settings in the environment (or in a .env file in its working folder, which is read on start).

On shutdown (SIGTERM) it stops taking new work and waits up to 2 minutes for live phone calls to finish.

Settings

Every setting is an environment variable. Paths are relative to the folder the API runs in.

Core

VariableDefaultWhat it does
PUBLIC_URLhttp://localhost:8100The address people and services reach the API at. Links in messages, Stripe returns and Twilio webhooks all use it, and Twilio signatures are checked against it, so it must match exactly. An https:// address also turns on secure cookies.
PORT8100Port to listen on
HOST127.0.0.1Address to listen on
NODE_ENVdevelopmentSet production in production
DATA_DIR./dataDatabase, uploads, site versions, backups and generated secrets
DB_FILE<DATA_DIR>/jagg.dbThe SQLite database
APP_SECRETgeneratedSecret for hashing visitor addresses and signing internal tokens. Generated once into <DATA_DIR>/app-secret if not set. Keep it stable: changing it drops phone calls in progress and restarts the day's visitor count.
ADMIN_EMAILnoneThe account with this email becomes the operator, with access to the admin tools (after setting up two-step sign-in)
TRUST_PROXYfalseSet true behind a proxy, so client addresses come from CF-Connecting-IP or X-Forwarded-For. Rate limits depend on it.
DEV_ORIGINShttp://localhost:5173Extra origins allowed to make signed-in dashboard requests, comma-separated. For a dashboard dev server.

Hosted sites and domains

VariableDefaultWhat it does
SITES_DOMAINlocalhostHosted sites live at <slug>.<SITES_DOMAIN>
SITES_SCHEMEhttps if PUBLIC_URL is https, else httpScheme used in hosted-site links
SITES_PORTempty for https, else 8200Port used in hosted-site links
CUSTOM_DOMAIN_TARGET<slug>.<SITES_DOMAIN>The CNAME target shown to owners for custom domains
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_IDnoneWhen set, custom domains are registered as Cloudflare custom hostnames

AI

VariableDefaultWhat it does
OPENROUTER_API_KEYnoneWithout it, the built-in practice AI answers instead
OPENROUTER_BASE_URLhttps://openrouter.ai/api/v1Any OpenAI-compatible chat completions endpoint with tool calling and streaming
AI_MODEL_CHATanthropic/claude-haiku-5.5Web chat and texts
AI_MODEL_VOICEanthropic/claude-haiku-5.5Phone calls
AI_MODEL_ASSISTANTanthropic/claude-haiku-5.5The owner's assistant in the dashboard
AI_FAKEfalseForce the practice AI even with a key, for tests and demos

Email

VariableDefaultWhat it does
SENDGRID_API_KEYnoneSend all email through Twilio SendGrid. When set, SMTP_URL isn't used.
SMTP_URLnoneAn SMTP connection URL, like smtps://user:[email protected]:465, used when there's no SendGrid key. Without either, emails are written to <DATA_DIR>/dev-mail instead of sent.
MAIL_FROMJust a Good Guy <hello@localhost>The From address. Customer emails keep this address but show the business's name, with Reply-To set to the business's own email.
INBOUND_EMAIL_DOMAINnoneThe domain customers email receptionists at: each business gets <slug>@<this domain>
INBOUND_EMAIL_SECRETnoneA long random string in the Inbound Parse webhook URL. Inbound email is off unless this and the domain are both set.
SENDGRID_API_BASEhttps://api.sendgrid.comFor testing against a fake

Email through Twilio SendGrid

One-time setup in SendGrid:

  1. API key. Create one under Settings, API Keys with Mail Send access. Put it in SENDGRID_API_KEY.
  2. Sender. Under Settings, Sender Authentication, authenticate the domain in MAIL_FROM (for example justagoodguy.us): add the DNS records SendGrid shows, then verify. Until a domain or single sender is verified, SendGrid refuses to send, and the outbox shows that error.
  3. Customers emailing receptionists (optional). Pick a subdomain you don't use for your own mail, like mail.justagoodguy.us. Add an MX record for it pointing at mx.sendgrid.net (priority 10). Under Settings, Inbound Parse, add that subdomain with the URL PUBLIC_URL/api/webhooks/email/<INBOUND_EMAIL_SECRET>. Leave "POST the raw, full MIME message" off; turning on the spam check is a good idea. Then set INBOUND_EMAIL_DOMAIN and INBOUND_EMAIL_SECRET.

Each business's address then shows in its Inbox. Owners forward their business email there, and the receptionist answers. See Email.

Phone and texts (Twilio)

VariableDefaultWhat it does
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKENnoneWithout them, texts are logged instead of sent, number search returns practice numbers, and signature checks are skipped
TWILIO_FROMnoneFallback sender for texts not sent from a business's own number
TWILIO_MESSAGING_SERVICE_SIDnoneSend those texts through a Messaging Service instead (used before TWILIO_FROM)
TWILIO_API_BASEhttps://api.twilio.comFor testing against a fake
TWILIO_WA_TEMPLATE_CONFIRMnoneOptional approved WhatsApp template (Content SID, HX...) for booking confirmations sent more than 24 hours after the customer's last WhatsApp message
TWILIO_WA_TEMPLATE_REMINDERnoneThe same for reminders
TWILIO_PRIMARY_CUSTOMER_PROFILE_SIDfound automaticallyYour approved Primary Business Profile in Trust Hub (BU...), needed to register businesses for texting. See texting registration
TWILIO_A2P_EMAILthe support email, then ADMIN_EMAILWhere Twilio sends notices about each business's texting registration
TWILIO_A2P_MOCKfalseRegister free mock brands that can't send, for a dry run
TWILIO_TRUSTHUB_BASE, TWILIO_MESSAGING_BASETwilio'sFor testing against a fake

Numbers bought through the dashboard are pointed at PUBLIC_URL/api/twilio/voice, /api/twilio/voice/status and /api/twilio/sms automatically. Calls use Twilio ConversationRelay over a WebSocket at PUBLIC_URL/api/voice/relay.

WhatsApp

WhatsApp uses each business's own Twilio number and the same keys as texts. One-time setup in the Twilio console:

  1. Register the sender. Under Messaging, Senders, WhatsApp senders, register the business's Twilio number as a WhatsApp sender (this needs a Meta Business account, and the display name should be the business's name).

  2. Point it at us. Set the sender's "a message comes in" webhook to PUBLIC_URL/api/twilio/sms (POST) and its status callback to PUBLIC_URL/api/twilio/sms/status. Texts and WhatsApp share this webhook.

  3. Templates (optional). WhatsApp only allows normal messages within 24 hours of the customer's last message. To send confirmations and reminders after that, create templates in the Content Template Builder using exactly these variables, submit them for WhatsApp approval (category: Utility), and put their Content SIDs in the two settings above:

    • {{1}} business name, {{2}} service, {{3}} date and time, {{4}} link to change or cancel
    • For example: "{{1}}: you're booked for {{2}} on {{3}}. Change or cancel: {{4}}"

    Without templates, those messages go out as normal texts instead. See WhatsApp.

Billing (Stripe)

Stripe is only used to bill businesses for their plan and add-ons.

VariableDefaultWhat it does
STRIPE_SECRET_KEYnoneWithout it, billing is off and every account stays fully on
STRIPE_WEBHOOK_SECRETnoneSigning secret for PUBLIC_URL/api/webhooks/stripe (subscriptions: checkout.session.completed, customer.subscription.*, invoice.paid, invoice.payment_failed)

Stripe events are deduplicated, and subscriptions are re-checked against Stripe every hour in case a webhook was missed.

Other

VariableDefaultWhat it does
VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEYgeneratedWeb push keys. Generated once into <DATA_DIR>/vapid.json if not set.
VAPID_SUBJECTmailto:hello@localhostContact for push services
TURNSTILE_SITE_KEY, TURNSTILE_SECRETnoneCloudflare Turnstile. When both are set, a visitor sending more than 15 chat messages in 10 minutes from one address is asked for a quick person check (once an hour). Normal visitors never see it.
DASHBOARD_DIR../dashboard/distBuilt dashboard
WIDGET_DIR../widget/distBuilt widget
DOCS_DIR../../docs/distBuilt docs
SITE_DIR../../siteLanding page

Behind a proxy

If you put nginx or similar in front:

  • Turn response buffering off and allow long reads. Chat replies stream as Server-Sent Events, the host app long-polls for up to 50 seconds, and dashboard live updates stay open.
  • Pass WebSocket upgrades through, for phone calls at /api/voice/relay.
  • Allow request bodies of at least 9 MB. Site uploads arrive in 8 MB pieces.
  • Set TRUST_PROXY=true, and make sure the proxy sets X-Forwarded-For (or CF-Connecting-IP behind Cloudflare).

Data and backups

Everything lives in DATA_DIR. A copy of the database is written to <DATA_DIR>/backups once a day (around 08:00 UTC), and the last 7 are kept. Copy that folder somewhere else as well.

Housekeeping runs on its own: expired sessions and codes are removed, and raw analytics events older than 18 months are deleted.

Search the docs