Mirra
Function Client (self-host)

Self-host quickstart

This runs Function Client on one machine with Docker: the server, its databases, object storage and the web client. When it's up, your team opens one address in a browser.

You need:

  • Docker with Compose 2.20 or newer (docker compose version): Docker Desktop, or colima on a Mac. Give Docker at least 4 GB of memory.
  • An Anthropic API key, from console.anthropic.com. The assistant runs on it.
  • To serve a team, a machine they can reach and a domain whose DNS you control. On a laptop, neither.
  • On Windows, WSL 2. Every command here is bash: run them in a WSL shell (Ubuntu), with Docker Desktop's WSL integration turned on for that distribution, and clone into the WSL filesystem (~/client, not under /mnt/c). Under /mnt/c, chmod 600 silently does nothing, so the settings file would stay readable by everyone, and builds are slow. Docker's memory there is WSL's, set in %UserProfile%\.wslconfig ([wsl2] then memory=4GB).

Sign-in uses this server's own email-and-password accounts, so there is no third-party account to set up. Sign in with Privy covers signing in with Privy instead.

1. Get the code and the settings file

git clone https://github.com/functionprotocol/client.git
cd client
cp apps/server/.env.selfhost.example .env.selfhost
chmod 600 .env.selfhost

.env.selfhost holds every secret the server has. Only you should be able to read it. It is listed in .gitignore; keep it that way.

2. Fill in the required block

Open .env.selfhost. Fill the empty values in the block marked required to boot:

NameWhat to put there
GRAPH_PASSopenssl rand -hex 16. Also Neo4j's password, so at least 8 characters.
JWT_SECRETopenssl rand -hex 32. Every login is signed with it, so at least 32 characters.
ENCRYPTION_MASTER_KEYopenssl rand -hex 32. Exactly 64 hex characters. It encrypts every connected integration's credentials: back it up (Backup and restore), and never change it.
INTERNAL_REQUEST_SECRETopenssl rand -hex 32
ANTHROPIC_API_KEYyour Anthropic API key

Leave the rest of that block as it is. Compose sets the database, graph, Redis and storage addresses itself.

Then add these lines to the file:

# The object store's key and secret: letters and digits only.
S3_BUCKET_ACCESS_KEY=<openssl rand -hex 8>
S3_BUCKET_SECRET=<openssl rand -hex 32>

Replace each <…> with the command's output.

On a server your team reaches

Point DNS for your domain and for pages.<your domain> at the machine. Pages that people publish run on that second host, so they can never read the app's session. Then add:

FUNCTION_DOMAIN=client.example.com
FUNCTION_SCHEME=https

Step 4 makes the server public with EDGE_BIND=0.0.0.0, once you have your account. The edge container then obtains the certificates itself, so ports 80 and 443 must be open to the internet. Never set EDGE_BIND=0.0.0.0 without FUNCTION_SCHEME=https: passwords would cross the network in the clear.

On a laptop, skip this. The address is http://localhost.

3. Start it

docker compose --env-file .env.selfhost up -d --build

The first build takes several minutes. Pass the same --env-file to every later compose command (ps, logs, down), or compose stops, saying GRAPH_PASS is unset.

To follow the server as it starts:

docker compose --env-file .env.selfhost logs -f server

The boot log prints one mirra.<subsystem>=<mode> line for each subsystem: mirra.auth=local, mirra.email=none, mirra.billing=none and so on. If a required value is missing, the server exits and names every missing value at once. Some warnings are expected, one for each thing this setup leaves off: PUBLIC_PAGES_HOST is unset (see Reverse proxy), mirra.script_runtime=unavailable until you turn on flows, and Agora, push notifications, OPENAI_API_KEY and GOOGLE_TENOR_KEY not set.

4. Make yourself the admin

The first account on the server is its admin, and until one exists, whoever reaches the address first can take it. On a laptop, open http://localhost, choose Create an account, and go on to step 5.

On a server, a new domain is public as soon as its certificate is issued, so create the account before the edge listens to the internet. On the machine itself, run the lines below, with your email in place of you@example.com. At the blank prompt, type your password (8 or more characters, no double quote or backslash) and press Enter.

read -rs PW
printf '{"email":"%s","password":"%s"}' you@example.com "$PW" \
  | curl -sS http://127.0.0.1:3000/api/auth/local/register -H 'Content-Type: application/json' --data @- \
  | grep -oE '"(role|error)":"[^"]*"'
unset PW

It prints "role":"admin" when your account exists. (If you set FUNCTION_PORT, use that port instead of 3000.)

Then add EDGE_BIND=0.0.0.0 to .env.selfhost and run docker compose --env-file .env.selfhost up -d. Sign in at https://<FUNCTION_DOMAIN>. If registering ever answers Sign-up is closed on this server before you have an account, someone else got there first: run down -v and start again.

Sign-up closes after the first account, so open it while your team joins. Add this line to .env.selfhost:

LOCAL_SIGNUP=open

Then run up again. Compose recreates the server, because its settings changed, and leaves the rest running:

docker compose --env-file .env.selfhost up -d

Send your team the address. Each person opens it and creates an account. Once everyone has joined, set LOCAL_SIGNUP=off and run up -d again. While sign-up is open, anyone who can reach the address can make an account and spend your Anthropic key: with billing off (the default), nothing limits what an account spends.

Your team uses the client in the browser from here. To serve the client from a CDN of your own as well, see Getting the client to your team.

Turn on flows

Flows and scripts are off: deploying one answers 503. The built-in flows (scheduled flows, batch operations) need them. To turn them on, add this line to .env.selfhost and run up -d:

SCRIPT_RUNTIME=local

Each script then runs as a child process inside the server container, with network access and the server's own OS user. Anyone who can deploy a script can take over the server: read every value in .env.selfhost (JWT_SECRET and ENCRYPTION_MASTER_KEY included), read and change every account's data in the databases directly, and rewrite other people's scripts. Turn this on only if everyone who can sign in is someone you would hand the server to, and never while LOCAL_SIGNUP=open: set LOCAL_SIGNUP=off first. Under Privy sign-in, anyone who can sign in to your Privy app gets an account, so restrict sign-in in the Privy dashboard first (Sign in with Privy).

To let someone join later, remove SCRIPT_RUNTIME=local, set LOCAL_SIGNUP=open and run up -d; once they have an account, set LOCAL_SIGNUP=off, put SCRIPT_RUNTIME=local back and run up -d again. Flows don't run in between.

Call reminders and call summaries deploy too. They run during and after voice calls, and without live-call transcription (Voice calls) a reminder posts "No conversation detected" and a summary is skipped. The server's image already carries the SDK that scripts load.

Everything else is optional

apps/server/.env.selfhost.example describes each optional setting beside its commented-out line: email (SMTP), Stripe billing, the event bus, organizations, the crypto bundle, video rendering. Uncomment what you want, then run up -d.

Stop, update, remove

docker compose --env-file .env.selfhost down          # stop; data stays in volumes
git pull && docker compose --env-file .env.selfhost up -d --build   # update
docker compose --env-file .env.selfhost down -v       # stop and DELETE all data

Back up before an update (Backup and restore).

On this page