# Tel-Agent — environment template # # Copy this file to `.env` and fill in real values. # `.env` is gitignored and must never be committed. # # cp .env.example .env # # Rule: the code reads configuration from environment variables only. # Never assume Docker, and never hardcode a credential in a source file. # # --------------------------------------------------------------------------- # WHAT BELONGS IN THIS FILE — and what does not (spec B9.2) # --------------------------------------------------------------------------- # THIS FILE holds *installation* secrets: one set per installation, set once by # whoever runs the server, changed with a restart. # DATABASE_URL, REDIS_URL, ENCRYPTION_KEY, LiveKit keys. # # THE DATABASE holds *user-entered* credentials, encrypted at rest: provider # API keys, per-number SIP credentials, and messaging channel tokens. Those are # typed into the UI, take effect without a restart, and there can be many of # each. ENCRYPTION_KEY below is what encrypts them. # # .env is NOT the safer place for user credentials. It is plaintext on disk, so # a single file read exposes every one of them. Encrypted columns force an # attacker to obtain both the database and the key. # # MILESTONE 0 IS THE EXCEPTION. There is no database yet, so the provider keys # below live here. That ends at Milestone 2. Never write a settings screen that # edits this file. # --------------------------------------------------------------------------- # Telephony — how a call reaches the agent # # Two paths. Set exactly one of them. # # livekit A number bought from a SIP provider, pointed at a LiveKit Cloud # SIP trunk. This is what Milestone 0 uses. The agent connects # outwards only: no inbound ports, no NAT traversal, no RTP range. # sip Direct registration to a PBX extension (3CX, Asterisk, FreePBX). # The on-premises path. Run on the same LAN as the PBX. # --------------------------------------------------------------------------- TELEPHONY_MODE=livekit # --- TELEPHONY_MODE=livekit ------------------------------------------------ # From the LiveKit Cloud project settings. LIVEKIT_URL= LIVEKIT_API_KEY= LIVEKIT_API_SECRET= # The inbound trunk the provider number points at. LIVEKIT_SIP_TRUNK_ID= # The number in E.164, for logging and for matching routing rules later. INBOUND_NUMBER= # --- TELEPHONY_MODE=sip ---------------------------------------------------- # Hostname or IP of the PBX. Run Tel-Agent on the same LAN as the PBX so that # NAT and STUN stay out of the picture; that is the single biggest cause of # "the call connects but there is no audio". SIP_HOST=192.168.1.10 SIP_PORT=5060 # Extension number the agent registers as. SIP_EXTENSION=999 # SIP username and the auth ID / password from the extension's settings page. SIP_USERNAME= SIP_PASSWORD= # UDP port range for RTP media. On a server these must be open, and # `external_ip` must be set, or audio will not flow. RTP_PORT_MIN=10000 RTP_PORT_MAX=20000 # --- Both modes ------------------------------------------------------------ # G.711 (PCMU/PCMA) is easier to debug than Opus and has fewer compatibility # surprises. 8 kHz, mono. SIP_CODEC=PCMU # --------------------------------------------------------------------------- # Speech to text — Deepgram, streaming (Milestone 11, §B3) # # A key alone is enough: the model and language have working defaults. Like the # model above, this is the installer's / standalone path; §B9.2 puts a key entered # from the screen in an encrypted column, and that wins over this file. # --------------------------------------------------------------------------- DEEPGRAM_API_KEY= # The streaming model. `nova-2` is the current default; a call needs the low-latency # one, not the batch/quality tiers. DEEPGRAM_MODEL=nova-2 # Where Deepgram's streaming socket lives. Nobody changes this in production; tests # point it at a stand-in. DEEPGRAM_BASE_URL=wss://api.deepgram.com # Language of the calls. Austrian German is the primary target; test with real # names and addresses, which is where STT accuracy actually breaks down. STT_LANGUAGE=de # --------------------------------------------------------------------------- # Language model — pick the fastest provider you have access to. # # Leave LLM_PROVIDER empty and the agent still answers: it says the model is not # connected and that the message was stored. Set it and the other two become # required — a provider with no key behind it is refused rather than quietly # falling back, because that would look exactly like the model answering badly. # # `openai` means the API *shape* — POST /chat/completions with server-sent events — # and not one company's service. A hosted gateway or a model on your own machine # that speaks it is reached by pointing LLM_BASE_URL at it. # # **Settings → Advanced is the place now, not this file.** §B9.2 puts a key the user # enters in an encrypted database column, and a key saved there wins over anything set # here — per value, so an installation already running on this file can move one field # at a time. These four remain for two cases: an installer bringing a machine up before # anybody can sign in, and Milestone 11, where the agent runs as its own process with # no settings screen behind it. # # The reason the file is not the safer option: it is plaintext on disk, so one file # read exposes everything, while an encrypted column forces an attacker to obtain both # the database and ENCRYPTION_KEY. # --------------------------------------------------------------------------- LLM_PROVIDER= LLM_MODEL= LLM_API_KEY= LLM_BASE_URL=https://api.openai.com/v1 # --------------------------------------------------------------------------- # Text to speech — ElevenLabs, streaming (Milestone 11, §B3) # # Both the key and a voice are required: ElevenLabs has no default voice, and a call # with no voice cannot speak. Same §B9.2 story as the others - the screen's encrypted # value wins over this file. # --------------------------------------------------------------------------- ELEVENLABS_API_KEY= ELEVENLABS_VOICE_ID= # The synthesis model. `eleven_turbo_v2_5` is the low-latency one Rule 3's budget # needs; the quality-first models are too slow for a live call. ELEVENLABS_MODEL_ID=eleven_turbo_v2_5 # The audio codec streamed back. `ulaw_8000` is G.711 μ-law at 8 kHz - what a SIP call # carries (SIP_CODEC above), so the transport forwards it with no transcode on the # media path. ELEVENLABS_OUTPUT_FORMAT=ulaw_8000 # Where ElevenLabs lives. Nobody changes this in production; tests point it elsewhere. ELEVENLABS_BASE_URL=https://api.elevenlabs.io # --------------------------------------------------------------------------- # Agent behaviour # --------------------------------------------------------------------------- AGENT_NAME=Tel-Agent AGENT_LANGUAGE=de # Recording announcement. Austria requires both parties to be aware that a call # is recorded, and the requirement still applies once a human takes over. # Keep this on unless you have a specific legal reason not to. RECORDING_ANNOUNCEMENT=true # --------------------------------------------------------------------------- # Encryption — required from Milestone 2 onward # --------------------------------------------------------------------------- # 256-bit key, hex encoded, that encrypts every user-entered credential stored # in the database: provider API keys, per-number SIP credentials, and messaging # channel tokens. # # Generate with: openssl rand -hex 32 # # Lose this and every stored credential is unrecoverable. Leak it and the # encryption bought nothing. It must never be stored alongside a database dump. ENCRYPTION_KEY= # --------------------------------------------------------------------------- # The API service # --------------------------------------------------------------------------- # `development` allows the local dashboard origin and does not require an # encryption key. `production` requires ENCRYPTION_KEY and refuses to start # without it. ENVIRONMENT=development # Where conversations and transcripts are stored. Two dialects are supported # (D-029) and the driver must be the async one in both cases: # # sqlite+aiosqlite:///./tel-agent.db a file on this machine # postgresql+asyncpg://user:password@localhost/telagent your own Postgres # # A synchronous driver stalls the event loop under load and is refused at # startup rather than discovered in production. DATABASE_URL=sqlite+aiosqlite:///./tel-agent.db # Origins allowed to call the API with credentials, comma separated. The # dashboard runs on its own port in development. A wildcard is refused: it is # what people reach for when CORS is in the way, and it ships. CORS_ORIGINS=http://localhost:38471 # Host headers this installation answers to, comma separated. Add the hostname # from the install wizard once it is behind a reverse proxy. TRUSTED_HOSTS=localhost,127.0.0.1 # --------------------------------------------------------------------------- # Request limits # --------------------------------------------------------------------------- # Ceilings on one request, against a mistake rather than an attacker: an # unbounded body and an unbounded handler are each a denial of service that # nobody has to intend. Nothing in this API accepts an upload, so a megabyte is # far above every body it takes. MAX_BODY_BYTES=1048576 # Measured to the *first byte* of the response, never to the last: a stream is # then free to run for as long as somebody is reading it. Raising this does not # make a slow query fast; it makes the process wait longer before saying so. REQUEST_TIMEOUT_SECONDS=30 # Strict-Transport-Security # --------------------------------------------------------------------------- # Seconds, and 0 means the header is not sent. Leave it at 0 unless this # installation is reached over TLS and always will be: a browser that sees this # header refuses plain HTTP to the host for that long, and nothing the server # does afterwards takes it back. 31536000 is a year. HSTS_SECONDS=0 # Where the server listens # --------------------------------------------------------------------------- # Loopback, so the port is not reachable from the network. The three supported # ways to reach an installation from elsewhere all talk to a server on # 127.0.0.1: a private network, a VPN, or a reverse proxy terminating TLS. # # Changing this exposes the port to everything that can route to this machine, # and `python -m api` says so in a warning when you do. There is no # authentication in front of it other than the product's own. BIND_HOST=127.0.0.1 BIND_PORT=38472 # --------------------------------------------------------------------------- # Scheduled tasks and background jobs # --------------------------------------------------------------------------- # The installation's own clock, run inside the API process: housekeeping, # health probes and outbound email. Turn it off only where a second process # already runs it — exactly one clock should tick per installation. JOBS_ENABLED=true # --------------------------------------------------------------------------- # Database pooling — PostgreSQL only; SQLite has no server to pool against # --------------------------------------------------------------------------- DATABASE_POOL_SIZE=5 DATABASE_MAX_OVERFLOW=10 DATABASE_POOL_TIMEOUT=30 # Echo every SQL statement. Useful once, noisy always. DATABASE_ECHO=false # --------------------------------------------------------------------------- # The public address of this installation — optional # --------------------------------------------------------------------------- # Scheme and host only, no trailing slash: https://desk.example.com # # Leave it empty on a machine that is reached directly. Set it when a reverse # proxy terminates TLS in front of this installation, because two things then # have to agree on an address neither of them can see: the Channels tab prints # a channel's public webhook address, and the public door verifies a signature # the platform computed over the address it called. Without this the card # prints http://127.0.0.1:38472/... while the platform signed https://your # host/..., and every delivery is refused. PUBLIC_BASE_URL= # --------------------------------------------------------------------------- # Mail — optional. Unset SMTP_HOST and the forgot-password screen honestly # says this installation cannot send email, instead of pretending one is on # the way. SMTP_FROM is required alongside SMTP_HOST. # --------------------------------------------------------------------------- SMTP_HOST= SMTP_PORT=587 SMTP_USERNAME= SMTP_PASSWORD= SMTP_FROM= SMTP_USE_TLS=true SMTP_USE_SSL=false # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- LOG_LEVEL=INFO # --------------------------------------------------------------------------- # Docker Compose only - ignored by a manual run # --------------------------------------------------------------------------- # docker-compose.yml reads this file for ${...} substitutions. Everything below # has a working default for an installation used on the machine that runs it: # dashboard on http://localhost:38471, API on http://localhost:38472, both # published on loopback only, SQLite on the tel-agent-data volume. # # Reaching the installation from other machines means three changes, then # `docker compose up -d --build` (the dashboard bakes its API address at build # time, so it must be rebuilt): # # TEL_AGENT_PUBLIC_API_URL=http://your-host:38472 # TEL_AGENT_WEB_ORIGIN=http://your-host:38471 # TEL_AGENT_TRUSTED_HOSTS=localhost,127.0.0.1,your-host # TEL_AGENT_API_LISTEN=0.0.0.0:38472 # TEL_AGENT_WEB_LISTEN=0.0.0.0:38471 # # The postgres profile (docker compose --profile postgres up -d) additionally # wants: # # TEL_AGENT_POSTGRES_PASSWORD= # TEL_AGENT_DATABASE_URL=postgresql+asyncpg://telagent:@db/telagent # --------------------------------------------------------------------------- # Telegram channel (Milestone 3) # --------------------------------------------------------------------------- # The bot token itself is NOT set here - it is user-entered, stored encrypted in # the database, and saved from the Channels tab (see the two-kinds rule above). # This base URL exists for development and tests, which point it at a stand-in # for the Bot API. Production installations never change it. TELEGRAM_API_BASE=https://api.telegram.org # Where Meta's Graph API lives (WhatsApp channel, Milestone 3). The customer's # credentials are NOT set here - they are entered on the Channels card and stored # encrypted. This base exists for development and tests only. WHATSAPP_API_BASE=https://graph.facebook.com/v23.0 # Where Meta's Graph API lives for Messenger and Instagram (Milestone 3). Separate # from WHATSAPP_API_BASE so a test can stand in for one product without the other. # Customer credentials are entered on the Channels cards, stored encrypted. META_API_BASE=https://graph.facebook.com/v23.0 # Discord's REST API and Slack's Web API (Milestone 3). Both transports dial out - # the Discord gateway and Slack Socket Mode addresses are fetched from these, so a # stand-in for either base controls both halves of its transport in development and # tests. Customer tokens are entered on the Channels cards, stored encrypted. DISCORD_API_BASE=https://discord.com/api/v10 SLACK_API_BASE=https://slack.com/api