Skip to content
OpenScribe brand logo Docs

Self-Hosting & OpenShip Deployment Guide

OpenScribe packages its Go + PocketBase backend (server/), React web interface (web/dist/), public documentation (web/dist/docs/), and remote Model Context Protocol (MCP) server into a single stateless container image that stores all persistent state on one mounted volume (/pb/pb_data).

You can run OpenScribe locally from a clean Git checkout, inside a standalone Docker container, or on a self-hosted OpenShip instance.


1. Environment Variables Reference (.env & .env.required)

All runtime secrets are injected via environment variables. The repository root includes .env.required listing the expected environment variable names. When running locally, create .env and restrict file permissions to owner-only (chmod 600 .env). Never commit .env to version control.

VariableRequiredDefaultDescription
PUBLIC_URLYeshttp://127.0.0.1:8090Canonical external origin without a trailing slash (for example https://scribe.example.com). Used to construct Google OAuth callback URLs, device pairing verification links (/pair), MCP OAuth metadata issuers, and canonical URLs in /docs/, /robots.txt, and /sitemap.xml.
PORTNo8090TCP port where the Go + PocketBase HTTP server listens inside the process or container.
PB_DATA_DIRNo./pb_data (local) / /pb/pb_data (Docker)Directory path holding data.db, auxiliary.db, and uploaded meeting attachments. Must be backed by a persistent volume in container deployments.
OPENSCRIBE_ADMIN_EMAILYesNoneComma-separated Google email allowlist. Only verified accounts on this list can create an account or sign in. An empty list denies application access. Calendar connections may use other Google accounts.
PB_ADMIN_EMAILYesFalls back to the first address in OPENSCRIBE_ADMIN_EMAILEmail address for the PocketBase _superusers account (/_/). Bootstrapped automatically on startup by openscribe prepare and ensureSuperuserBootstrap (server/auth.go).
PB_ADMIN_PASSWORDYesNone (10+ characters)Password for the PocketBase _superusers account (minimum 10 characters). Setting both PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD before first boot prevents PocketBase from exposing an unauthenticated web installer token.
PB_ENCRYPTION_KEYYes (Docker / prepare)None (exact 32 characters)Exact 32-character ASCII key passed via --encryptionEnv=PB_ENCRYPTION_KEY so PocketBase encrypts OAuth client secrets and application settings at rest inside SQLite (server/auth.go and docker-entrypoint.sh).
OPENSCRIBE_TOKEN_ENCRYPTION_KEYRecommendedDerived fallback in dev32-byte key (64 hex characters from openssl rand -hex 32 or 43-44 base64 characters) used for AES-256-GCM encryption of calendar_accounts.encrypted_token (server/crypto.go). Set explicitly in production so rotating GOOGLE_CLIENT_SECRET never invalidates stored calendar refresh tokens.
GOOGLE_CLIENT_IDYes (for OAuth & Calendar)NoneGoogle OAuth 2.0 Web Application Client ID (*.apps.googleusercontent.com). Synced into the PocketBase users collection on startup (applyRuntimeSettings in server/auth.go) and used by /api/openscribe/calendar/connect.
GOOGLE_CLIENT_SECRETYes (for OAuth & Calendar)NoneGoogle OAuth 2.0 Web Application Client Secret (GOCSPX-...). Kept strictly on the server.
ELEVENLABS_API_KEYOptional (default ASR)NoneServer-held ElevenLabs API key used by POST /api/openscribe/transcription/session (server/elevenlabs.go and server/transcription/) to mint short-lived, single-use realtime_scribe tokens (scribe_v2_realtime). Never sent to clients.
OPENAI_API_KEYOptionalNoneServer-held OpenAI API key for AI workloads assigned to the openai provider (default gpt-6-luna for tagging and profile_enrichment) and for openai_realtime (relayed via GET /api/openscribe/transcription/openai-realtime with token = "" so OpenAI credentials never leave the server) or openai_compatible transcription when selected.
ANTHROPIC_API_KEYOptionalNoneServer-held Anthropic API key for AI workloads assigned to the anthropic provider (default claude-sonnet-5-5 for qa and summary).
GOOGLE_API_KEYOptionalNoneServer-held Google Gemini API key (GEMINI_API_KEY also accepted for ASR) for AI workloads assigned to the google provider (default gemini-3.8-flash for action_items) and for gemini_realtime transcription tokens (uses: 1, model-locked bidiGenerateContentSetup).
OPENAI_COMPATIBLE_TRANSCRIPTION_BASE_URLOptionalNoneBase URL (for example http://127.0.0.1:8080/v1) for an OpenAI-compatible /v1/audio/transcriptions endpoint proxied by POST /api/openscribe/transcription/audio (data_destination = "server_proxy_http"). Optional bearer token via OPENAI_COMPATIBLE_TRANSCRIPTION_API_KEY.
TYPESAFE_API_KEYOptionalNoneServer-held Typesafe key for bounded jev-1.14.0 search suggestions. Only the query and candidate labels are sent. Local matching works without it.
SMTP_URLOptionalNoneOptional SMTP connection URL for transactional emails.
MAIL_FROMOptionalNoneOptional sender address when SMTP_URL is configured.

Generating Encryption Keys

Generate PB_ENCRYPTION_KEY (exact 32 hex characters) and OPENSCRIBE_TOKEN_ENCRYPTION_KEY (64 hex characters, or 32 decoded bytes) with OpenSSL:

bash
# 32-character ASCII key for PB_ENCRYPTION_KEY
openssl rand -hex 16

# 64-hex-character (32-byte) key for OPENSCRIBE_TOKEN_ENCRYPTION_KEY
openssl rand -hex 32

2. Running Locally from a Clean Checkout

You can build and run OpenScribe directly on Linux or macOS without any private analysis directories or external scripts.

Step 1: Prepare .env

bash
touch .env
chmod 600 .env

Edit .env (referring to .env.required) and set PUBLIC_URL=http://127.0.0.1:8090, OPENSCRIBE_ADMIN_EMAIL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, PB_ENCRYPTION_KEY, OPENSCRIBE_TOKEN_ENCRYPTION_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and your AI provider keys.

Step 2: Build the Public Documentation and React Frontend

The Go server automatically serves static files from ../web/dist (or ./pb_public inside Docker). Running npm run build in web/ automatically executes npm run build:docs (node ../docs/build-docs.mjs) to generate /docs/, /robots.txt, and /sitemap.xml before bundling the React frontend:

bash
cd web
npm ci
PUBLIC_URL=http://127.0.0.1:8090 npm run build
cd ..

Step 3: Build and Start the Go + PocketBase Server

Always pass --dev=false so PocketBase never prints SQL statements or sensitive parameters to standard output:

bash
cd server
go build -o openscribe .
./openscribe prepare --dev=false --dir="${PB_DATA_DIR:-./pb_data}" --encryptionEnv=PB_ENCRYPTION_KEY
./openscribe serve --dev=false --http="127.0.0.1:${PORT:-8090}" --dir="${PB_DATA_DIR:-./pb_data}" --encryptionEnv=PB_ENCRYPTION_KEY

3. Standalone Docker Deployment

The root Dockerfile performs a reproducible multi-stage build:

  1. frontend (node:22-alpine): Copies README.md, docs/, server/API.md, and web/, installs frontend dependencies (npm ci), removes any prebuilt public/docs, public/robots.txt, and public/sitemap.xml, runs npm run build (build:docs && tsc -b && vite build) using ARG PUBLIC_URL=http://__openscribe_public_url__.invalid by default, and verifies that dist/docs/index.html, dist/robots.txt, and dist/sitemap.xml were generated.
  2. backend (golang:1.27-alpine): Compiles the static Go binary (CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /openscribe .).
  3. Runtime (alpine:3.23): Installs ca-certificates and tzdata, copies /pb/openscribe, /pb/pb_public (containing the built web app and /docs/), and /pb/docker-entrypoint.sh, exposes port 8090, and defines a container HEALTHCHECK against http://127.0.0.1:8090/api/openscribe/config.

Build and Run with Docker

bash
docker build -t openscribe:latest .

docker volume create openscribe-data

docker run -d \
  --name openscribe \
  --restart unless-stopped \
  -p 127.0.0.1:8090:8090 \
  --env-file .env \
  -e PB_DATA_DIR=/pb/pb_data \
  -v openscribe-data:/pb/pb_data \
  openscribe:latest

How docker-entrypoint.sh Bootstraps the Server

On every container start, docker-entrypoint.sh:

  1. Sets DATA_DIR (${PB_DATA_DIR:-/pb/pb_data}) and PORT (${PORT:-8090}).
  2. Verifies that PB_ENCRYPTION_KEY is exactly 32 characters, PB_ADMIN_EMAIL (falling back to the first address in OPENSCRIBE_ADMIN_EMAIL) is set, and PB_ADMIN_PASSWORD is at least 10 characters.
  3. Reads the build-time origin sentinel (BUILD_ORIGIN) from Sitemap: <BUILD_ORIGIN>/sitemap.xml inside pb_public/robots.txt and rewrites BUILD_ORIGIN to the runtime TARGET_ORIGIN (${PUBLIC_URL:-http://127.0.0.1:${PORT}} without trailing slash) across pb_public (*.html, robots.txt, and sitemap.xml).
  4. Runs database migrations, applies OAuth and app settings, and upserts the bootstrap superuser via:
    sh
    ./openscribe prepare --dev=false --dir="$DATA_DIR" --encryptionEnv=PB_ENCRYPTION_KEY
  5. Replaces the shell process (exec) with:
    sh
    exec ./openscribe serve --dev=false --http="0.0.0.0:${PORT}" --dir="$DATA_DIR" --encryptionEnv=PB_ENCRYPTION_KEY

4. Deploying with OpenShip (openship.json)

OpenScribe includes a validated openship.json manifest at the repository root:

json
{
  "$schema": "https://openship.io/openship.schema.json",
  "framework": "docker",
  "runtime": "docker",
  "port": 8090,
  "volumes": [
    "openscribe-data:/pb/pb_data"
  ],
  "domains": [
    {
      "domain": "scribe.akyonlabs.com",
      "port": 8090
    }
  ]
}

Adapting openship.json and .openship/project.json for Your Own Domain

When deploying OpenScribe to your own OpenShip instance:

  1. Edit openship.json and replace "scribe.akyonlabs.com" under domains with your own domain (for example "scribe.example.com").
  2. Bind .openship/project.json to your own OpenShip project using openship project create and openship init --force --project <your-project-id> as shown below.

Step-by-Step OpenShip CLI Workflow

1. Verify OpenShip Context and Validate openship.json

bash
openship --json context list
openship --json status
openship --json config validate

2. Create and Bind Your OpenShip Project

If you are deploying to your own OpenShip server for the first time, create a project and bind .openship/project.json to your project ID:

bash
openship project create --name openscribe
openship project list
openship init --force --project <your-project-id>

3. Configure Secret Environment Variables

Use openship project env set <your-project-id> --secret so API keys and OAuth secrets are stored encrypted in OpenShip:

bash
openship project env set <your-project-id> \
  --set PUBLIC_URL=https://scribe.example.com \
  --set PORT=8090 \
  --set PB_DATA_DIR=/pb/pb_data \
  --set OPENSCRIBE_ADMIN_EMAIL=you@example.com \
  --set PB_ADMIN_EMAIL=you@example.com

openship project env set <your-project-id> --secret \
  --set PB_ADMIN_PASSWORD="$PB_ADMIN_PASSWORD" \
  --set PB_ENCRYPTION_KEY="$PB_ENCRYPTION_KEY" \
  --set OPENSCRIBE_TOKEN_ENCRYPTION_KEY="$OPENSCRIBE_TOKEN_ENCRYPTION_KEY" \
  --set GOOGLE_CLIENT_ID="$GOOGLE_CLIENT_ID" \
  --set GOOGLE_CLIENT_SECRET="$GOOGLE_CLIENT_SECRET" \
  --set ELEVENLABS_API_KEY="$ELEVENLABS_API_KEY" \
  --set OPENAI_API_KEY="$OPENAI_API_KEY" \
  --set ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
  --set GOOGLE_API_KEY="$GOOGLE_API_KEY"

Verify the configured environment keys (secret values remain masked):

bash
openship project env get <your-project-id>

4. Attach and Verify Your Custom Domain

Point your DNS A / AAAA or CNAME record for scribe.example.com to your OpenShip host, then register and verify the domain:

bash
openship domain add scribe.example.com --project <your-project-id>
openship domain list --project <your-project-id>
openship domain verify <domain-id>

5. Deploy and Watch Build Logs

Trigger the OpenShip deployment (which builds the Dockerfile, runs npm run build:docs during the frontend stage, and updates canonical URLs at container startup via docker-entrypoint.sh):

bash
openship deploy --watch

Check deployment history or inspect failure logs at any time:

bash
openship deployment list --project <your-project-id>
openship deployment logs <deployment-id>

5. Persistent Storage & Backup Policy (/pb/pb_data)

Because openship.json mounts "openscribe-data:/pb/pb_data", all stateful files survive container rebuilds and redeployments:

  • /pb/pb_data/data.db (+ -wal and -shm): Primary SQLite database containing users, calendar_accounts (with AES-256-GCM encrypted_token), oauth_transactions, meetings, meeting_series, meeting_participants, transcript_segments, meeting_notes, meeting_attachments, people_profiles, tags, ai_settings, device_logins, and MCP OAuth tables (mcp_oauth_clients, mcp_oauth_codes, mcp_oauth_tokens).
  • /pb/pb_data/auxiliary.db: PocketBase internal logs and metrics.
  • /pb/pb_data/storage/: Uploaded meeting screenshots, attachments, and replay bundles.

Always back up /pb/pb_data together with both PB_ENCRYPTION_KEY and OPENSCRIBE_TOKEN_ENCRYPTION_KEY. Without those keys, encrypted PocketBase settings and encrypted Google Calendar refresh tokens in calendar_accounts cannot be decrypted after a restore.


6. Post-Deploy Verification Checklist

Run the following checks against your deployed PUBLIC_URL (replace https://scribe.example.com with your domain or http://127.0.0.1:8090 locally):

1. Public Config & Readiness (GET /api/openscribe/config)

OpenScribe exposes its health and capability status at GET /api/openscribe/config (used by the Dockerfile HEALTHCHECK):

bash
curl -sS https://scribe.example.com/api/openscribe/config

Expected 200 OK JSON response:

json
{
  "app_name": "OpenScribe",
  "public_url": "https://scribe.example.com",
  "google_oauth_enabled": true,
  "google_client_id": "123456789012-abc.apps.googleusercontent.com",
  "elevenlabs_configured": true,
  "ai_providers": {
    "openai": true,
    "anthropic": true,
    "google": true
  }
}

2. Public Documentation, robots.txt, and sitemap.xml

Verify that /docs/, /robots.txt, and /sitemap.xml are publicly reachable without authentication and that unknown paths under /docs/ return HTTP 404:

bash
curl -sS -o /dev/null -w "%{http_code}\n" https://scribe.example.com/docs/
curl -sS -o /dev/null -w "%{http_code}\n" https://scribe.example.com/docs/self-hosting/
curl -sS -o /dev/null -w "%{http_code}\n" https://scribe.example.com/robots.txt
curl -sS -o /dev/null -w "%{http_code}\n" https://scribe.example.com/sitemap.xml
curl -sS -o /dev/null -w "%{http_code}\n" https://scribe.example.com/docs/nonexistent-page

Expected status codes: 200, 200, 200, 200, and 404.

3. Admin AI Workload & Transcription Routing (GET / PUT /api/openscribe/admin/*)

Once signed in with an authorized Google account listed in OPENSCRIBE_ADMIN_EMAIL, inspect or update the AI provider and model ID for each workload (tagging, profile_enrichment, qa, summary, action_items) as well as the default transcription provider and model (elevenlabs, openai_realtime, gemini_realtime, or openai_compatible):

bash
curl -sS https://scribe.example.com/api/openscribe/admin/models \
  -H "Authorization: Bearer $OPENSCRIBE_USER_TOKEN"

curl -sS https://scribe.example.com/api/openscribe/transcription/providers \
  -H "Authorization: Bearer $OPENSCRIBE_USER_TOKEN"

7. Remote MCP Server (POST /api/openscribe/mcp)

OpenScribe includes a built-in Streamable HTTP Model Context Protocol (MCP) server (server/mcpbridge/ and server/mcpauth/) secured with OAuth 2.1 + PKCE so AI assistants (Claude, ChatGPT, Cursor, Codex, and custom MCP clients) can query your meetings with owner-scoped permissions.

Copy the MCP server URL from Settings → Apps & connections in the web workspace. Add that URL in your assistant’s connector settings, then sign in to OpenScribe when it asks. Web connectors need a publicly reachable HTTPS address. A localhost workspace works with local clients on the same machine.

Discovery & OAuth 2.1 Endpoints

EndpointMethodPurpose
/.well-known/oauth-protected-resourceGETRFC 9728 Protected Resource Metadata pointing to <PUBLIC_URL>/api/openscribe/mcp.
/.well-known/oauth-protected-resource/api/openscribe/mcpGETPath-scoped RFC 9728 Protected Resource Metadata.
/.well-known/oauth-authorization-serverGETRFC 8414 OAuth 2.1 Authorization Server Metadata (code_challenge_methods_supported: ["S256"]).
/api/openscribe/oauth/registerPOSTRFC 7591 Dynamic Client Registration for public MCP clients (token_endpoint_auth_method: "none").
/api/openscribe/oauth/authorizeGETOAuth 2.1 authorization endpoint (requires code_challenge and code_challenge_method=S256).
/api/openscribe/oauth/consentPOSTAuthenticated user approval or denial for an MCP client authorization request.
/api/openscribe/oauth/tokenPOSTExchanges authorization_code + code_verifier or refresh_token for an owner-scoped MCP Bearer token.
/api/openscribe/oauth/revokePOSTRevokes an issued MCP access or refresh token.
/api/openscribe/oauth/grantsGET / DELETELists or revokes active MCP client grants for the signed-in user.
/api/openscribe/mcpPOST / GET / DELETEStreamable HTTP MCP JSON-RPC 2.0 endpoint. Returns 401 Unauthorized with WWW-Authenticate: Bearer resource_metadata="<PUBLIC_URL>/.well-known/oauth-protected-resource" when called without a valid token.

Available MCP Tools (server/mcpbridge/bridge.go)

Every MCP tool enforces owner = <authenticated_user_id> and blocks sensitive financial or compensation queries (CheckNoFinancialData in server/intelligence/):

  1. meetings_list (MeetingsListInput): List and filter meetings owned by the authenticated user using account_ids, emails, tag_ids, series_id, q, verification_mode, and limit (1-200).
  2. meetings_search (MeetingsSearchInput): Search across meeting titles, summaries, and speaker-attributed transcript segments using query, series_id, account_ids, tag_ids, and limit (1-100).
  3. meeting_read (MeetingReadInput): Read a specific meeting occurrence (meeting_id), returning transcript segments, participants, notes, links, screenshot OCR text, and recurring series occurrences (with MCP App UI resource ui://openscribe/meeting-viewer.html).
  4. meeting_screenshot_read (MeetingScreenshotReadInput): Read a specific meeting-window screenshot attachment (meeting_id, attachment_id), returning the image content and metadata (dimensions, timestamp, window title, source app, and OCR text).
  5. meeting_ask (MeetingAskInput): Ask a grounded question against a specific meeting (meeting_id, question, include_series_history, include_screenshots, attachment_ids, max_images) and receive an answer with transcript segment and screenshot citations.
  6. context_metadata (ContextMetadataInput): Read recurring series metadata, participant profiles (with manual overrides and source provenance), and user-defined tags owned by the authenticated user (series_id, person_id, email).