Skip to content

Operator guide

This guide is for whoever runs the Me server: today, a Windows PC with the local install (ADR 0023). People who only use Me need Getting started.

Terminal window
pwsh deploy/local/install.ps1 [-Root <folder>] [-Port <n>] [-Services postgres,temporal] [-TunnelHostname <host>] [-NoStart]

Run it from your own terminal, in the repo root. It is idempotent: re-running it after git pull publishes the new build and keeps the secrets, data, tokens, port and tunnel.

Parameter Default What it does
-Root %LOCALAPPDATA%\Me Where to install. Use a folder outside %LOCALAPPDATA% when you run the script inside a packaged app, such as a Claude Code session in the desktop app, where Windows redirects that folder.
-Port 5041 The server’s loopback port. Windows’ Connected Devices Platform service holds 5040. A re-run keeps an existing install’s port unless you pass -Port.
-Services postgres, temporal Which containers to start. postgres alone runs without workflows: they are refused, and everything else works.
-TunnelHostname (none) A public hostname, such as me.example.com, in a zone of your Cloudflare account, for webhooks. A re-run keeps it.
-NoStart off Stop after writing the settings (step 3 below): no containers, no scheduled tasks, no tunnel.

What it does, in order:

  1. Generates secrets once: the database password and the kernel’s ES256 call-token signing key, in config\secrets.json. The config folder is restricted to you and SYSTEM, by SID.
  2. Publishes the server, each production plugin and the me CLI into -Root. The CLI is JIT-compiled, so the machine needs no Visual Studio C++ tools.
  3. Writes the server’s settings. server\appsettings.Production.json holds the connection string, the plugin list, the kernel URL and the signing key. With -TunnelHostname it also holds Me:WebhookBaseUrl, even with -NoStart. The file is restricted the same way. With -NoStart, the script stops here.
  4. Checks the images and starts the containers. It confirms each image exists, pulls it if needed, and starts Postgres and Temporal with podman compose from deploy/local/compose.yaml.
  5. Registers the scheduled task “Me server” (at logon, restarts up to 5 times, one minute apart) and starts it. Neither the task nor the settings set ASPNETCORE_ENVIRONMENT, so the server runs as Production: Me:CallTokenPrivateKey is required and Me:DevAuth is refused.
  6. Sets up the tunnel, with -TunnelHostname.

Then, once, create the first tenant and log in (see Getting started):

Terminal window
& "<Root>\server\Me.Server.exe" setup --tenant "<name>" --owner "<you>" [--days 90] [--another]

setup refuses to run once a tenant exists, unless you pass --another, which adds a separate tenant.

Plugins installed. The production plugins only; simulators and samples are left out. com.me.claude isn’t in the list yet. The full list is the $plugins array in install.ps1.

Images. ME_POSTGRES_IMAGE overrides the pinned Postgres image, for example to use one already pulled. If a pull fails with proxyconnect tcp, a proxy leaked into the Podman machine. The script prints the fix: podman machine ssh 'env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy podman pull <image>'.

Piece Where Notes
Server (kernel + API) <Root>\server\Me.Server.exe, scheduled task Me server 127.0.0.1:<Port>
Plugins <Root>\plugins\<Project>\ Started and supervised by the server, each on its own loopback port, with database credentials for its own schema only
CLI <Root>\cli\me.exe Put it on PATH
Postgres 17 Podman, compose project me-local 127.0.0.1:55432, volume me-local_me-local-postgres
Temporal Podman, compose project me-local 127.0.0.1:7243, UI at http://127.0.0.1:8243, volume me-local_me-local-temporal
Tunnel (optional) cloudflared, scheduled task Me tunnel Exposes /webhooks/* only

Everything binds to loopback. The only way in from outside is the tunnel, and it carries only webhooks.

Terminal window
Stop-ScheduledTask 'Me server'; Start-ScheduledTask 'Me server' # restart; allow ~40 s
podman ps --filter name=me-local # the containers

A graceful stop takes the plugin processes with it. Stop-Process -Force on the server leaves them running.

Providers that push events need a public address: Telnyx (SMS, receipts, calls), Cloudflare mail, Graph mail, and Microsoft Graph change notifications for Teams and Microsoft 365 mail (without the tunnel, those two fall back to polling every 2 minutes). -TunnelHostname sets up a named Cloudflare Tunnel, which keeps the same address across restarts:

Terminal window
winget install Cloudflare.cloudflared
cloudflared tunnel login # once: opens the browser; pick the zone of your hostname
pwsh deploy/local/install.ps1 -TunnelHostname me.example.com

The script then:

  1. Creates the tunnel me-local (or reuses it).
  2. Routes DNS for the hostname to it. It never overwrites an existing record: if one exists, it warns, and the record must be a CNAME to <tunnel id>.cfargotunnel.com.
  3. Writes config\cloudflared.yml. Its ingress passes only paths shaped like /webhooks/conn_<26 characters>[/…], with no dot segments, so /webhooks/../v1/… can’t reach the API. Everything else answers 404, and the API, the plugin API and OAuth stay on loopback.
  4. Registers the scheduled task “Me tunnel” and starts it.

The public address, Me:WebhookBaseUrl, was already written with the settings (step 3 of the install). me integration add prints each new connection’s full webhook URL from it. me integration list doesn’t show webhook URLs, so note the URL when you add the connection.

Coming soon: -TunnelHostname installs cloudflared and runs its login for you (PR #141).

On main, whoever connects Microsoft 365 registers a public-client Entra app (no secret) and passes its client id. The client id is a setting of each connection, so several people can share one app by passing the same id. See the Microsoft 365 guide.

Coming soon: the Microsoft app becomes operator infrastructure. The operator registers it once (with a register-microsoft-app.ps1 script), and people only sign in, without a client id (PR #140). This section will describe the script once it is merged.

There is no backup command yet.

  • A verified backup exists in code (KernelBackup). It dumps the database, restores the dump into a scratch database, and checks there that every migration is applied and every tenant’s audit chain is unbroken. Nothing calls it outside the tests.
  • me backup create|verify|restore is specified (spec 05) but not built.
  • The manual procedure below skips both checks.

What to keep:

What Where Why
The database Postgres volume Everything: kernel and plugin schemas, the encryption key ring for connection secrets, and stored files (artifacts)
<Root>\config\secrets.json The install The database password and the call-token signing key
Temporal’s data Volume me-local_me-local-temporal In-flight workflow runs and their timers

A database backup with Postgres’ own tools, from PowerShell (checked on a live install, including a restore into a scratch database):

The Postgres container’s name depends on the compose provider (me-local_postgres_1 with podman-compose, me-local-postgres-1 with docker-compose), so look it up once, by its compose labels:

Terminal window
$pg = podman ps --filter label=com.docker.compose.project=me-local --filter label=com.docker.compose.service=postgres --format '{{.Names}}'
$stamp = Get-Date -Format yyyyMMdd-HHmm
podman exec $pg pg_dump -U me_owner -d me --format=custom -f /tmp/me.dump
podman cp "${pg}:/tmp/me.dump" "D:\Backups\me-$stamp.dump"
podman exec $pg rm /tmp/me.dump

Check that a backup restores, into a scratch database that you then drop:

Terminal window
podman cp "D:\Backups\me-$stamp.dump" "${pg}:/tmp/me.dump"
podman exec $pg createdb -U me_owner me_restore_check
podman exec $pg pg_restore -U me_owner --no-owner --exit-on-error -d me_restore_check /tmp/me.dump
podman exec $pg psql -U me_owner -d me_restore_check -tAc "select count(*) from kernel.tenants"
podman exec $pg dropdb -U me_owner me_restore_check
podman exec $pg rm /tmp/me.dump

The dump holds every secret’s ciphertext and the key ring that decrypts it, so store it as carefully as secrets.json.

Restoring onto a fresh Postgres isn’t fully covered yet (milestone M9):

  • The kernel’s own roles (me_app, me_system) are created only by the first migration, and the restored public.me_schema_versions marks it applied, so the server doesn’t recreate them, and pg_restore --exit-on-error stops at the first grant to them. Create them before restoring: podman exec $pg psql -U me_owner -d postgres -c "CREATE ROLE me_app NOLOGIN; CREATE ROLE me_system NOLOGIN BYPASSRLS; GRANT me_app, me_system TO me_owner;"
  • Each plugin’s login role is created only when the server provisions that plugin.
  • --no-owner leaves every restored table owned by me_owner, so the plugin tables lose their owners.

The server reads <Root>\server\appsettings.Production.json, which install.ps1 writes. Re-running the script rewrites the file, so keep your own changes in a note and re-apply them, or pass them as environment variables in the ASP.NET form (Me__PublicUrl, for example).

Key Set by install.ps1 Meaning
ConnectionStrings:Kernel yes Postgres, as the owner role
Kestrel:Endpoints:Http:Url yes http://127.0.0.1:<Port>
Me:KernelUrl yes Where plugins call the kernel back
Me:ContractsPath yes The contract files (<Root>\server\contracts)
Me:CallTokenPrivateKey yes ES256 key that signs every kernel→plugin call. Required outside Development
Me:Plugins yes The plugins to launch, each with Id, FileName, Arguments, WorkingDirectory and Environment (install.ps1 leaves out Arguments). A plugin starts from an empty environment, apart from a few system variables (PATH, TEMP, USERPROFILE…), so a variable reaches it only through its Environment
Me:DevAuth false Development headers. Refused outside Development
Me:PluginInstallApi false Installing plugins over the API. Off by default
Me:WebhookBaseUrl with -TunnelHostname The public origin for /webhooks/<conn>
Me:PublicUrl no A hosted server’s public origin. The OAuth redirect becomes <PublicUrl>/v1/oauth/callback, and the sign-in page then shows whose system an account would join before connecting it. A server not on loopback refuses sign-ins without it
Me:Coverage:<plugin id> no Raises what a provider may receive, to Confidential or Restricted, once a BAA covers it (D-45). Without it, nothing above Internal goes to that plugin’s model or channel
Me:Approvals:Origin, Me:Approvals:RpId, Me:PublicBaseUrl no Passkey approvals: the WebAuthn origin (default https://localhost), the relying-party id (default: the origin’s host) and the base URL of approval links (default: the origin, without a trailing /). Me:PublicBaseUrl is not Me:PublicUrl, which sets the OAuth redirect
Me:MigrateOnStartup no (default true) Apply database migrations at start
Me:RunBackgroundLoops no (default true) Run the kernel’s dispatch, timer and relay loops in this process
Variable Read by Meaning
ME_CONFIG me The CLI’s config file (default %APPDATA%\me\config.json). One per login, such as one for you and one for an agent
ME_TOKEN me A token to use instead of the stored one
ME_OUTPUT me json or human
ME_NO_BROWSER me integration add Print sign-in links instead of opening the browser
ME_CLIENT_SESSION me An id that groups me calls into one session (for example, one per agent session)
ME_SCOPE me Continues an interaction scope, such as the overlay that me attention open returns
ME_POSTGRES_IMAGE install.ps1 Another Postgres image
ME_DB_PASSWORD deploy/local/compose.yaml Required when you run podman compose by hand: the dbPassword from secrets.json. install.ps1 sets it only while compose runs
ME_WEBHOOK_URL Cloudflare mail worker (wrangler.toml) The connection’s webhook URL (Cloudflare mail)
ME_TEAMS_POLL_SECONDS Teams plugin Poll interval when there is no push (default 120, minimum 30). Set it in the plugin’s Environment
ME_MAIL_POLL_SECONDS M365 mail plugin Poll interval when there is no push (default 120, minimum 30). Set it in the plugin’s Environment
ME_CLAUDE_PATH Claude model plugin The claude executable for subscription tokens (default: claude on the server user’s PATH; not a batch file). Set it in the plugin’s Environment
ME_TEMPORAL_ADDRESS, ME_TEMPORAL_NAMESPACE Workflow plugin Temporal’s address (install: 127.0.0.1:7243) and namespace (default default)

The server sets ME_PLUGIN_ID, ME_KERNEL_URL, ME_PLUGIN_DB, ME_KERNEL_PUBLIC_KEY and ASPNETCORE_URLS for each plugin after its Environment, so values for them in Me:Plugins are overwritten.