Configuration reference

Every FIKADESK_* variable the api parses, grouped, with defaults and what unset means.

The api reads its configuration from environment variables, parsed by a Zod schema in apps/api/src/env.ts and the module schemas it composes. An invalid value fails the boot with the offending field name, so a typo never survives to the first request.

The unset-means-off pattern

Many features are gated on an optional variable. Compose writes an unset variable through as an empty string, and the schemas read empty as absent, so the legal states are the ones a deployment without that feature runs in. An empty FIKADESK_EMAIL_DOMAIN means no email channel, not an email channel that breaks. An empty FIKADESK_LLM_AGENT_MODEL means the agent sink is absent, so no visitor message reaches a model. Features that are still being wired in follow the same shape: the Meta adapter is live-pending, so all four META_* variables default to absent and no Meta channel exists until they are set.

Some variables are not optional, because a deployment cannot be ambiguous about them. DATABASE_URL, FIKADESK_AUTH_SECRET, FIKADESK_PUBLIC_URL and FIKADESK_REALTIME_SECRET are required, and a boot without them names the field that is missing.

Core

VariableDefaultWhat it does
FIKADESK_ROLEapiWhich process this image runs. api, worker, scheduler, ingest or migrate. The one-shot migration job uses migrate; no long-running service carries it.
PORT3000The port the api listens on.
FIKADESK_HOST0.0.0.0 in production, 127.0.0.1 otherwiseThe listen address. Only a production container sits behind Caddy, so dev and e2e runs stay off the network.
DATABASE_URLrequiredThe Postgres connection string the api and worker use, through PgBouncer.
DATABASE_URL_DIRECTDATABASE_URLA direct connection the outbox drainer uses. It holds a session advisory lock and a LISTEN, which a transaction pooler cannot carry across statements.
LOG_LEVELinfoOne of fatal, error, warn, info, debug, trace.
FIKADESK_TRUSTED_PROXIESemptyComma-separated addresses or CIDR ranges whose forwarding headers the api believes. Empty means a direct deployment reads the socket address and nothing else.
FIKADESK_REALTIME_SECRETrequired, 32+ charactersSigns the realtime subscription token, which Centrifugo verifies with the same key. Its own secret on purpose: the realtime server must not be able to forge a session.
FIKADESK_REALTIME_URLhttp://127.0.0.1:8000Where the api reaches Centrifugo. The browser never does; the api bridges the stream.

Auth

VariableDefaultWhat it does
FIKADESK_AUTH_SECRETrequired, 32+ charactersSigns sessions and tokens. Generate it with openssl rand -hex 32. Changing it signs everyone out.
FIKADESK_PUBLIC_URLrequiredThe absolute URL people open. Cookies, email links and the passkey relying party come from it, and it is the origin the api accepts cookie-bearing requests from.
FIKADESK_TRUSTED_ORIGINSthe four development localhost originsExtra absolute origins allowed to call the api with a cookie, comma separated. Unset is not the same as empty: unset falls back to the development origins, which a deployment must not carry. Set it empty to trust nothing.
FIKADESK_ALLOW_SIGNUPfalseWhen false, sign-up is invitation-only and workspace creation is limited to instance administrators.
FIKADESK_SETUP_TOKENunsetA fixed one-time setup token. Unset, the api prints one while the users table is empty.
FIKADESK_EE_PATHunsetPath the api loads enterprise extensions from. The OSS image builds with ee/ deleted, so nothing needs this.
FIKADESK_GOOGLE_CLIENT_ID / FIKADESK_GOOGLE_CLIENT_SECRETunsetGoogle social sign-in. Set both or neither; one without the other fails the boot.
FIKADESK_GITHUB_CLIENT_ID / FIKADESK_GITHUB_CLIENT_SECRETunsetGitHub social sign-in, same both-or-neither rule.
FIKADESK_MICROSOFT_CLIENT_ID / FIKADESK_MICROSOFT_CLIENT_SECRETunsetMicrosoft social sign-in, same both-or-neither rule.

Channels and storage

VariableDefaultWhat it does
FIKADESK_CHANNELS_SECRETunset32 hex bytes that seal channel provider credentials at rest with AES-256-GCM. Unset, the api cannot create an api channel.
FIKADESK_MEDIA_URLhttp://localhost:3000/mediaThe origin attachments upload to and serve from, their own origin so a customer file never runs on the inbox session. Picking it later breaks every URL already embedded in a conversation.
FIKADESK_MESSENGER_ORIGINunsetWhere the messenger frame is served when that is not the api's own origin. Production serves both from one host and needs none of this.
FIKADESK_STORAGE_ROOT.storageFilesystem path the object store keeps files under when no bucket is configured.
FIKADESK_STORAGE_BUCKETuploadsThe object store bucket name.
FIKADESK_IMPORT_MAX_BYTES2147483648 (2 GiB)The largest import archive the resumable upload accepts.
FIKADESK_IMPORT_SECRETunset32 hex bytes that seal hosted-source importer tokens at rest. Unset, the CSV importer still works but no hosted source can be created or run.
FIKADESK_OUTBOX_SHARDS1How many shards the outbox splits into, when more than one drainer runs.
FIKADESK_OUTBOX_SHARD0Which shard this drainer owns. Must be below FIKADESK_OUTBOX_SHARDS.
FIKADESK_CAMPAIGN_RATE_PER_SECOND10The campaign fan-out's send ceiling in messages per second. A workspace sending to a large audience never fires faster than this.

Email

VariableDefaultWhat it does
FIKADESK_EMAIL_TRANSPORTsmtpWhich inbound transport this deployment runs. An unknown name stops the process at boot rather than at the first send.
FIKADESK_EMAIL_DOMAINunsetThe domain this instance owns. Every workspace sends and receives at <email key>@<domain> with no DNS work. Unset, together with the secret, is the no-email-channel state.
FIKADESK_EMAIL_SECRETunset32 hex bytes. Encrypts the DKIM private keys and signs the reply token.
FIKADESK_EMAIL_SMTP_URLunsetOutbound submission, smtps://user:pass@host:465. Without it the channel receives and never sends.
FIKADESK_EMAIL_SMTP_LISTEN127.0.0.1:2525Where the SMTP receiver listens. It is not an MX; point the box's own MTA at it.
FIKADESK_EMAIL_SMTP_ALLOW127.0.0.1/32,::1/128The peers the receiver accepts, addresses or CIDR ranges. Anything else is refused at connect, before a body streams.
FIKADESK_EMAIL_SPF_INCLUDEunsetThe token the domain wizard asks a customer to add inside their existing v=spf1 record. Unset, it asks for the egress addresses instead.
FIKADESK_EMAIL_EGRESS_IPSemptyThe addresses this instance sends from, used for the SPF record when there is no include token.
FIKADESK_EMAIL_BOUNCE_HOSTunsetThe CNAME target of the return-path record a customer publishes, so bounces come back here.
FIKADESK_EMAIL_AUTHSERV_IDunsetThe authserv-id your MTA stamps on the Authentication-Results header it writes. Only that header is believed; unset means no inbound verdict is believed.
FIKADESK_EMAIL_MAX_BYTES31457280 (30 MB)Refused at the envelope before a body is read.
FIKADESK_EMAIL_OUTBOUNDsmtpThe outbound transport. smtp is a relay you supply; postmark and ses send over each provider's HTTP API.
POSTMARK_SERVER_TOKENunsetPostmark outbound credential. Unset, the postmark transport refuses to send.
POSTMARK_MESSAGE_STREAMunsetThe Postmark message stream the transport sends on.
POSTMARK_WEBHOOK_TOKENunsetA header token the Postmark delivery webhook must carry; set it or the basic-auth pair below.
POSTMARK_WEBHOOK_USER / POSTMARK_WEBHOOK_PASSunsetBasic-auth credentials for the Postmark delivery webhook.
SES_ACCESS_KEY / SES_SECRET_KEY / SES_REGIONunsetSES outbound credentials. Unset, the ses transport refuses to send.

AI agent

VariableDefaultWhat it does
FIKADESK_LLM_AGENT_MODELunsetThe model that answers visitors, written as provider:model. Unset, or with no embedding endpoint, the agent sink is absent and no visitor message reaches a model.
FIKADESK_LLM_CLASSIFIER_MODELunsetThe classifier tier's model. Falls back to the agent model.
FIKADESK_LLM_ESCALATION_MODELunsetThe escalation tier's model. Falls back to the agent model.
FIKADESK_LLM_ANTHROPIC_API_KEYunsetCredential for anthropic: models.
FIKADESK_LLM_OPENAI_API_KEYunsetCredential for openai: models.
FIKADESK_LLM_OPENAI_COMPATIBLE_API_KEYunsetCredential for an OpenAI-compatible endpoint.
FIKADESK_LLM_OPENAI_COMPATIBLE_BASE_URLunsetBase URL of the OpenAI-compatible endpoint.
FIKADESK_LLM_EMBEDDING_MODELunsetThe embedding model, always served by the OpenAI-compatible endpoint, so a plain model name the /embeddings call sends as-is.

Meta

VariableDefaultWhat it does
META_APP_IDunsetNames the Meta app the webhook subscription belongs to.
META_APP_SECRETunsetSigns every Meta webhook. Unset, or with no verify token, no Meta channel exists at all.
META_VERIFY_TOKENunsetThe shared handshake secret that answers Meta's subscription check.
META_WHATSAPP_TOKENunsetSends WhatsApp messages and downloads media. Without it, WhatsApp outbound is off.

Billing

Billing is optional and env-gated. With no provider configured the billing routes stay unmounted and the meter job never registers.

VariableDefaultWhat it does
FIKADESK_BILLING_PROVIDERunsetstripe or paddle. Unset, the image infers it from whichever key is present. Both providers configured without naming one fails the boot.
FIKADESK_STRIPE_SECRET_KEY / FIKADESK_STRIPE_WEBHOOK_SECRETunsetStripe credentials. The webhook secret verifies Stripe's signature.
FIKADESK_STRIPE_METER_SEATS / FIKADESK_STRIPE_METER_CONVERSATIONS / FIKADESK_STRIPE_METER_AI_RESOLUTIONSunsetThe event_name of each Stripe meter, not the mtr_ id.
FIKADESK_STRIPE_PLANSunsetComma-separated name:priceId pairs the auth plugin builds its plans from.
PADDLE_API_KEY / PADDLE_WEBHOOK_SECRETunsetPaddle credentials.
PADDLE_ENVsandboxsandbox or live.
PADDLE_METER_SEATS / PADDLE_METER_CONVERSATIONS / PADDLE_METER_AI_RESOLUTIONSunsetThe price id each metric maps to.
PADDLE_PLANSunsetComma-separated name:priceId pairs.

Compose stack

These variables are read by deploy/compose/compose.yml, not by the api's schema. They shape the stack itself. The full template with comments lives at deploy/compose/.env.example.

VariableDefaultWhat it does
FIKADESK_IMAGEghcr.io/fikadesk/fikadesk:latestThe image to run. Pin a release tag; upgrading is changing this line.
POSTGRES_PASSWORD / FIKADESK_APP_PASSWORDemptyOwner and app role passwords. Generate each with openssl rand -hex 24.
FIKADESK_BIND127.0.0.1The address Caddy publishes on. 0.0.0.0 opens it to the world.
FIKADESK_HTTP_PORT / FIKADESK_HTTPS_PORT80 / 443The host ports Caddy publishes, kept separate so the stack can share a box.
FIKADESK_DOMAINemptyThe public hostname. Empty serves plain HTTP; set it and point DNS here and Caddy fetches a certificate.
FIKADESK_UPSTREAM_PROXIESemptyAddresses and CIDR ranges of a proxy or CDN in front of Caddy, space separated. Empty means Caddy believes no forwarding header.
FIKADESK_FRONTEND_SUBNET / FIKADESK_FRONTEND_POOL / FIKADESK_CADDY_ADDRESS172.30.0.0/24 / 172.30.0.128/25 / 172.30.0.2The internal bridge Caddy, the api and the worker share. Change all three together, only if the subnet collides.
FIKADESK_SMTP_PORT2525The host port the ingest container publishes SMTP on.

Backups

Backups run pgBackRest. With no backup variables set, WAL archiving and the scheduled backups go to a local pgbackrest volume you copy off the box yourself. Set all five PGBACKREST_REPO1_S3_* variables to point the repository at S3-compatible storage, or all four R2_* variables to add a second repository on Cloudflare R2. The backup container refuses to start unless every variable in a set is present.

S3 repository
PGBACKREST_REPO1_S3_ENDPOINT=https://s3.example.com
PGBACKREST_REPO1_S3_BUCKET=fikadesk-backups
PGBACKREST_REPO1_S3_REGION=us-east-1
PGBACKREST_REPO1_S3_KEY=<access key>
PGBACKREST_REPO1_S3_KEY_SECRET=<secret key>
R2 second repository
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_BUCKET=fikadesk-backups
R2_ACCESS_KEY=<access key>
R2_SECRET_KEY=<secret key>