Skip to main content

Self-hosting Tiep

This guide deploys the clinician app, administrator platform, API, Redis worker, and the infrastructure they need. It is intended for an operator who owns the domains, Firebase project, Langfuse project, and AI-provider credentials.

There are two supported full-stack options:

OptionBest forYou operate
Bundled infrastructureA single server or evaluation environmentDocker runs PostgreSQL/pgvector, Redis, and MinIO in persistent volumes.
External infrastructureProduction environments with managed servicesYou provide PostgreSQL/pgvector, Redis, and S3-compatible object storage.

Frontend-only deployments are not covered here. They require an independently operated compatible API.

What you need​

Before starting, have the following ready:

  • A Linux host with Docker Engine and the Docker Compose plugin. The Docker daemon must be running.
  • Three DNS names: app.<domain>, platform.<domain>, and api.<domain>.
  • A reverse proxy that terminates TLS and can reach loopback ports on the host.
  • A customer-owned Firebase project, configured for browser authentication.
  • A customer-owned Langfuse project and its API keys.
  • An OpenAI or Groq API key for report generation, and a Groq or Fal key for transcription.
  • For the external option, PostgreSQL with pgvector support, Redis, and an S3-compatible bucket.

The deployment listens only on the host loopback interface:

ServiceHost addressPublic DNS name
Clinician app127.0.0.1:3001app.<domain>
Administrator platform127.0.0.1:3002platform.<domain>
API127.0.0.1:8080api.<domain>

Configure the reverse proxy to route each hostname to its matching port, terminate TLS, and preserve standard forwarded request headers. Do not publish these ports directly to the internet.

Architecture​

Choose one infrastructure option:

With the bundled option, PostgreSQL, Redis, and MinIO run as Docker services on this host. With the external option, the same containers connect to operator-managed equivalents.

Component descriptions​

ComponentResponsibility
Clinician appThe SvelteKit web application used by clinicians. It receives public Firebase browser configuration and sends authenticated requests to the API.
Administrator platformThe SvelteKit web application used to administer organizations, users, plans, and forms. It shares the same API and Firebase project.
Reverse proxyThe operator-managed entry point. It owns DNS and TLS, routes the three public hostnames to loopback ports, and should restrict operator-only diagnostic endpoints.
APIThe FastAPI service. It verifies Firebase bearer tokens, applies authorization and CORS, serves application requests, stores task records, and queues background work.
Redis workerA separate process that consumes Redis Stream tasks, performs long-running work such as transcription, retries failed tasks, and updates task status in PostgreSQL.
PostgreSQL with pgvectorThe system-of-record database for organizations, users, forms, consultations, reports, tasks, and task outbox records. pgvector support is required by the application schema.
Redis StreamsThe durable task-dispatch channel between the API and worker. Consumer-group acknowledgments and recovery handle interrupted worker deliveries.
S3-compatible storageStores uploaded consultation audio. Bundled mode uses MinIO; external mode uses an operator-managed compatible bucket.
Customer FirebaseProvides browser authentication and issues bearer tokens. The backend service account verifies tokens and manages Firebase users and custom claims.
Customer LangfuseStores and resolves the versioned AI prompts and receives tracing data. It must contain the bootstrapped prompt templates before AI requests succeed.
OpenAI, Groq, or FalOperator-selected BYOK AI services. OpenAI or Groq generates reports; Groq or Fal transcribes audio. Prompts, or audio for transcription, are sent to the selected provider.

Prepare Firebase​

Tiep uses Firebase Authentication for identity. The browser receives the Firebase web configuration; the API uses a separate, private service-account JSON file.

  1. In Firebase Console, open the customer project and enable the sign-in provider used for email-link login under Authentication → Sign-in method.

  2. Under Authentication → Settings → Authorized domains, add app.<domain> and platform.<domain>. Add local development hostnames only when you intend to use them.

  3. Under Project settings → General → Your apps, create or select a Web app and copy its Firebase configuration. It supplies the PUBLIC_FIREBASE_* variables below.

  4. Under Project settings → Service accounts, generate a new private key. Save it as firebase-service-account.json in this repository's root directory:

    tiep/
    ├── firebase-service-account.json
    ├── .env.self-hosted
    └── docker-compose.self-hosted.yaml

    This file is mounted only into the API, worker, and migration containers. It must belong to the same Firebase project as PUBLIC_FIREBASE_PROJECT_ID and must never be committed or placed in a frontend container.

Configure the environment​

Copy the tracked template and edit only the copy:

cp .env.self-hosted.example .env.self-hosted
chmod 600 .env.self-hosted firebase-service-account.json

.env.self-hosted is ignored by Git. Its values are supplied to all relevant containers at runtime.

Public URLs and Firebase web configuration​

Set these values in every deployment. The frontend containers fail at startup if required public values are missing.

VariableValue
PUBLIC_API_URLPublic API URL, for example https://api.example.com
PUBLIC_APP_URLPublic clinician app URL, for example https://app.example.com
PUBLIC_PLATFORM_URLPublic administrator platform URL, for example https://platform.example.com
PUBLIC_APP_ENVIRONMENTUsually production; use development only for a non-production instance
PUBLIC_FIREBASE_API_KEY through PUBLIC_FIREBASE_APP_IDValues from the Firebase Web app configuration
PUBLIC_FIREBASE_MEASUREMENT_IDOptional Firebase Analytics measurement ID
PUBLIC_FIREBASE_PROJECT_IDFirebase project ID; must match FIREBASE_PROJECT_ID
CORS_ALLOWED_ORIGINSComma-separated public frontend origins, for example https://app.example.com,https://platform.example.com

Firebase's web API key is browser configuration, not a service-account secret. Do not put the service-account JSON contents in any PUBLIC_* variable.

Backend, storage, and task settings​

These settings select the self-hosted execution path:

VariableRequired value
DEPLOYMENT_MODEself_hosted
TASK_DISPATCHERredis
FIREBASE_PROJECT_IDThe customer Firebase project ID
FIREBASE_CREDENTIALS_FILE/run/secrets/firebase-service-account.json
PATIENT_FIELD_ENCRYPTION_KEYSecret symmetric key used to encrypt sensitive patient fields stored in PostgreSQL
DATABASE_URLAsync PostgreSQL URL, such as postgresql+asyncpg://...
REDIS_URLRedis URL, such as redis://.../0
S3_ENDPOINT_URL, S3_REGION, S3_BUCKETReachable S3-compatible object storage details
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEYBucket credentials
TASK_MAX_ATTEMPTSOptional positive retry limit; defaults to 3
TASK_VISIBILITY_TIMEOUT_SECONDSOptional Redis message recovery timeout; defaults to 900

Audio is stored under sessions/<session-id>/<timestamp> in the configured S3 bucket. PostgreSQL tracks task state and Redis Streams dispatches background work to the worker.

Langfuse and AI credentials​

Langfuse is required for prompt resolution and tracing. Set LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, and LANGFUSE_SECRET_KEY for one customer Langfuse project.

For AI, choose one default generation provider and a matching supported model:

ProviderKeySupported models
openaiOPENAI_API_KEYGPT_5_MINI, gpt-4.1-2025-04-14, gpt-5.4-mini-2026-03-17, gpt-5.4-2026-03-05, gpt-5.5-2026-04-23
groqGROQ_API_KEYllama-3.1-8b-instant, llama-3.3-70b-versatile, openai/gpt-oss-120b

Set AI_DEFAULT_PROVIDER and AI_DEFAULT_MODEL to one row above. Also configure at least one transcription provider: GROQ_API_KEY or FAL_KEY. Vertex AI is unavailable in self-hosted mode.

Prompts and traces are sent to the configured Langfuse host. Audio sent to Groq or Fal for transcription leaves the deployment.

Optional observability and integrations​

Backend SendGrid, PostHog, Logfire, Sentry, and Intercom integrations are disabled when their variables are omitted. The self-hosted clinician app also leaves Sentry, PostHog, and Logfire uninitialized unless explicitly enabled with browser-facing configuration:

VariableEffect
PUBLIC_SENTRY_DSNInitializes browser Sentry with this public DSN.
PUBLIC_POSTHOG_API_KEYInitializes browser PostHog with this public project key. PUBLIC_POSTHOG_HOST is optional and defaults to https://eu.i.posthog.com.
PUBLIC_LOGFIRE_ENABLED=trueInitializes browser Logfire and proxies its telemetry through the API. Also set the private API LOGFIRE_TOKEN; never expose that token to the browser.

Leave these frontend variables empty to disable their browser SDKs. The administrator platform does not currently initialize these SDKs.

For secrets managed as files, mount the file into the relevant container and set the matching *_FILE variable, for example OPENAI_API_KEY_FILE=/run/secrets/openai_api_key. Do not commit secrets or operator-specific configuration.

Option A: Bundled infrastructure​

Use this option when one Docker host should run the entire stack. Docker creates persistent volumes for PostgreSQL, Redis, and MinIO.

  1. Complete Prepare Firebase and Configure the environment.

  2. Keep the bundled values from .env.self-hosted.example for DATABASE_URL, REDIS_URL, and S3_ENDPOINT_URL. Set strong, unique values for POSTGRES_PASSWORD, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY.

  3. Confirm Compose resolves the configuration before starting anything:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
    --profile bundled config
  4. Build and start the stack:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
    --profile bundled up --build -d --wait

    migrate exits successfully after applying database migrations. minio-init also exits successfully after creating the bucket; that is expected.

  5. Bootstrap the four repository-managed Langfuse prompts:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
    run --rm api /app/.venv/bin/python -m commands.bootstrap_langfuse_prompts
  6. Verify storage and service readiness:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
    run --rm api /app/.venv/bin/python -m commands.verify_storage

    curl -fsS http://127.0.0.1:8080/_internal/health/ready
    curl -fsS http://127.0.0.1:8080/_internal/health/deep
  7. Sign in once through the clinician or platform URL, then assign the first superadmin:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
    run --rm -it api /app/.venv/bin/python -m commands.createsuperuser

Option B: External infrastructure​

Use this option when PostgreSQL, Redis, and object storage are operated outside this Compose project.

  1. Provision infrastructure that the Docker host can reach:

    • PostgreSQL with pgvector support, a database, and an application user able to run migrations.
    • Redis reachable by both the API and worker.
    • An S3-compatible bucket with permissions to list the bucket and create, read, and delete objects.
  2. Complete Prepare Firebase and Configure the environment.

  3. Replace the bundled settings in .env.self-hosted with operator-managed endpoints and credentials. Use a postgresql+asyncpg:// DATABASE_URL, a Redis REDIS_URL, and an absolute http:// or https:// S3 endpoint URL. Prefer encrypted connections when services are outside the host network.

  4. Validate the configuration without the bundled profile:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml config
  5. Start the application services without --profile bundled:

    docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
    up --build -d --wait
  6. Run the Langfuse bootstrap, storage verification, readiness checks, and initial superadmin assignment from steps 5–7 of the bundled guide.

You own the availability, encryption, backup, restoration, upgrades, and access control of external PostgreSQL, Redis, and S3-compatible storage.

Day-two operations​

Health and logs​

EndpointPurpose
/_internal/health/liveProcess liveness only
/_internal/health/readyConfiguration, PostgreSQL, and Redis readiness; used by Docker health checks
/_internal/health/deepOn-demand S3, Firebase, and Langfuse diagnostic checks with timeouts

Use the API's loopback address locally or route these paths through an authenticated operator-only proxy location. Do not expose deep diagnostics publicly.

docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml logs -f api worker

Upgrade​

Back up first. Then pull or check out the intended version and rebuild the services. The migrate service runs before the API and worker start; to run migrations manually:

docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml run --rm migrate

After updating prompt templates, rerun the Langfuse bootstrap command. It creates changed prompts and leaves unchanged templates alone.

Backup and restore for bundled infrastructure​

Keep backups outside the Docker host and test restoration on a replacement deployment.

docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T postgres pg_dump -U tiep tiep > tiep.sql
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T minio sh -c 'tar -C /data -cf - .' > minio-data.tar

Restore only to a stopped or replacement deployment after verifying the backup:

docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T postgres psql -U tiep -d tiep < tiep.sql
docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled exec -T minio sh -c 'tar -C /data -xf -' < minio-data.tar

For external infrastructure, use the backup and restore procedures supplied by its operator.

Stop the deployment​

Stop containers while preserving bundled PostgreSQL, Redis, and MinIO data:

docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled down

Delete containers and all bundled persistent data only when intentionally resetting the instance:

docker compose --env-file .env.self-hosted -f docker-compose.self-hosted.yaml \
--profile bundled down --volumes