Developers
Advanced setup and infrastructure
For when the server leaves your laptop. Each topic is a diagram or a table, the shortest correct config, and a link to the full document on GitHub.
Four places it can run
The binary is the same in all four. What changes is the bind address, and with it how much auth and encryption you owe.

| Topology | Bind | Auth | TLS | Runs as |
|---|---|---|---|---|
| Laptop only | 127.0.0.1:49374 | None needed | No | systemd user unit, launchd agent, or a container |
| Homelab box | 0.0.0.0:49374 | Bearer token and host allowlist, both required | Recommended | Docker with a healthcheck, or the AUR system service |
| LAN with TLS | 0.0.0.0:49374 behind a proxy | Root token, plus a user and an API key per person | Yes. Caddy with its internal CA works without a domain | Docker Compose with a Caddy sidecar |
| Tunnel | No host port at all | Same as the LAN server | Yes, at Cloudflare’s edge | Docker Compose with a cloudflared sidecar |
A laptop can skip HTTP entirely with ai-memory serve --transport stdio. For a homelab, the repository ships a bin/deploy script with compose and env templates. Follow the homelab deploy walkthrough.
TLS through a reverse proxy
ai-memory does not terminate TLS itself, by design. A bearer token authenticates a request. It does not encrypt it.
You can skip TLS when
- the agent talks to it over stdio
- the server is loopback-only, for one user, and nobody opens
/webfrom another machine - it is local development or a one-off experiment
You need TLS when
- accounts exist, because
aim_keys then cross the network - the server is bound beyond loopback
- you open
/webfrom a different machine - the server is reachable beyond the LAN

For a public domain with ports 80 and 443 reachable. Caddy issues and renews the Let’s Encrypt certificate on its own. The repository has the full compose file as docker/compose.tls.caddy.yml.
memory.example.com {
reverse_proxy ai-memory:49374
}
No domain and nothing exposed. Caddy’s internal CA signs the certificate, and you install its root certificate once on every client machine. Skip that step and clients either refuse the connection or learn to click through warnings.
{
local_certs # tells Caddy to use the internal CA instead of LE
}
homelab.local, 192.168.1.50 {
reverse_proxy ai-memory:49374
}
docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt > caddy-root.crt
For when you already run nginx and have certificate files. HTTP/1.1 and the empty Connection header are required for MCP’s streamable HTTP transport.
server {
listen 443 ssl http2;
server_name memory.example.com;
ssl_certificate /etc/nginx/certs/memory.crt;
ssl_certificate_key /etc/nginx/certs/memory.key;
location / {
proxy_pass http://ai-memory:49374;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
An outbound-only tunnel, so no ports open on your router. It needs a domain on Cloudflare, and the free tier works. In the dashboard, point the public hostname at http://ai-memory:49374. The full compose file is docker/compose.tls.cloudflared.yml.
cloudflared:
image: cloudflare/cloudflared:latest
container_name: ai-memory-tunnel
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
Then tell the server
The allowlist must include the public hostname, or the DNS rebinding guard rejects the proxy’s requests. The secure cookie setting is required once people log in through a listener beyond loopback.
AI_MEMORY_AUTH_TOKEN=...long-random-token-from-generate-auth-token...
AI_MEMORY_AUTH__SECURE_COOKIE=true
AI_MEMORY_ALLOWED_HOSTS=memory.example.com,localhost,127.0.0.1
AI_MEMORY_BIND=0.0.0.0:49374
And repoint the clients
The MCP URL ends in /mcp. The hook URL is the bare origin.
ai-memory install-mcp --client claude-code --apply \
--server-url "https://memory.example.com/mcp" --auth-token "$AI_MEMORY_AUTH_TOKEN"
ai-memory install-hooks --agent claude-code --apply \
--server-url "https://memory.example.com" --auth-token "$AI_MEMORY_AUTH_TOKEN"
Read the full HTTPS guide, including subpaths and proxy timeouts for long bootstrap runs
Keep the server running
ai-memory does not restart itself. The service manager on each operating system does that.
For a single-user workstation. The AUR packages install the unit. It needs no sudo and keeps state in ~/.local/share/ai-memory. A user unit stops at logout unless you enable lingering.
systemctl --user daemon-reload
systemctl --user enable --now ai-memory.service
systemctl --user status ai-memory.service
journalctl --user -u ai-memory.service -f
# keep it running after you log out
loginctl enable-linger "$USER"
For a shared workstation or a LAN box. Data lives in /var/lib/ai-memory, config in /etc/ai-memory/config.toml, secrets in /etc/ai-memory/env. Do not run both units on the same bind address.
sudo systemd-sysusers /usr/lib/sysusers.d/ai-memory.conf
sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/ai-memory.conf
sudo -u ai-memory ai-memory \
--data-dir /var/lib/ai-memory \
--config /etc/ai-memory/config.toml \
init
sudo systemctl daemon-reload
sudo systemctl enable --now ai-memory.service
journalctl -u ai-memory.service -f
The macOS tarballs include a LaunchAgent plist with two placeholders. Run this from the extracted tarball. A LaunchAgent stops at logout, and macOS has no equivalent of lingering. Nothing rotates its two log files.
mkdir -p ~/Library/Logs/ai-memory
AI_MEMORY_BIN=~/Applications/ai-memory/ai-memory
sed -e "s|__AI_MEMORY_BIN__|$AI_MEMORY_BIN|" \
-e "s|__HOME__|$HOME|" \
packaging/launchd/com.github.akitaonrails.ai-memory.plist \
> ~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist
launchctl bootstrap gui/$(id -u) \
~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist
ai-memory has no Windows Service dispatcher, so sc create pointed at the exe does not work, and a Scheduled Task with Start-Process dies at the next reboot. WinSW wraps it. Use absolute paths: a service runs as LocalSystem, and %LOCALAPPDATA% would expand to the wrong profile.
<service>
<id>ai-memory</id>
<name>ai-memory MCP server</name>
<description>Long-term memory server for AI coding agents.</description>
<executable>C:\Users\you\AppData\Local\ai-memory\ai-memory.exe</executable>
<arguments>--data-dir "C:\Users\you\AppData\Local\ai-memory" serve --transport http --bind 127.0.0.1:49374</arguments>
<startmode>Automatic</startmode>
<onfailure action="restart" delay="5 sec"/>
<log mode="roll"/>
</service>
& "$Dest\ai-memory-service.exe" install
& "$Dest\ai-memory-service.exe" start
& "$Dest\ai-memory-service.exe" status
systemd units in the install guidelaunchd in the macOS guideWinSW in the Windows guide
The data directory and backups
The wiki is the truth. It is plain markdown in a git repository, and the search index is built from it.
Reindex rebuilds pages, links and full-text search from the wiki. It does not bring back sessions, observations, handoffs, users, keys, audit rows or embeddings, so the database still belongs in your backup.
| Directory | Holds | Kind | Back it up? |
|---|---|---|---|
wiki/ | Every page as markdown, in one git repository | Truth | Yes. It is in the backup tarball, and you can also rsync it or git push it |
raw/ | Immutable, sanitized transcript segments from managed launches | Truth | Yes, if you use ai-memory run. Copy it separately |
db/ | memory.sqlite: full-text index, entities, embeddings, sessions, users, audit rows | Mostly derived | Yes. Pages, links and search rebuild from the wiki. Sessions, handoffs, users and keys live only here |
models/ | The local embedding model, about 87 MB | Rebuildable | No. It downloads again, or you place the files yourself |
logs/ | Rolling trace output | Disposable | No |
Defaults: ~/.local/share/ai-memory on Linux, ~/Library/Application Support/ai-memory on macOS, %LOCALAPPDATA%\ai-memory on Windows, /data in the container. Override with AI_MEMORY_DATA_DIR.
Backup and restore

Back up
It uses SQLite’s online backup API, so writes during the snapshot stay coherent. The tarball holds the wiki tree, the database snapshot and config.toml.
# safe while the server runs
ai-memory backup --to /tmp/ai-memory-backup.tar.gz
Restore
--data-dir is the host-side path of the volume. The paths here are the ones from the deploy guide. Use yours.
# Stop the server first.
docker compose -f ~/deploy/ai-memory/docker-compose.yml down
# Restore (sysinfo refuses if the container is still running).
ai-memory restore --from /tmp/ai-memory-backup.tar.gz --data-dir /var/opt/docker/utils/ai-memory/data --force
# Start back up.
docker compose -f ~/deploy/ai-memory/docker-compose.yml up -d
Read lifecycle operations: purge, rename, move, restore a page, reset
Routing and identity
A shared server has to answer two questions: which project does this session belong to, and who is asking.
The marker file
By default the project is the name of the current directory, in a workspace called default. Drop a .ai-memory.toml in any ancestor directory to change that. Hooks walk up from the working directory and use the first marker they find.
Work and personal
One marker per parent directory. Every repository below it lands in that workspace, with the directory name as the project.
# ~/projects/movvia/.ai-memory.toml
workspace = "movvia"
# ~/personal/.ai-memory.toml
workspace = "personal"
Mono-repo
A marker with a project pins every subdirectory to that one project. The closest marker wins.
# ~/projects/movvia/pe-portais/.ai-memory.toml
workspace = "movvia"
project = "pe-portais"
Git worktrees
Linked worktrees and subdirectories resolve to the main repository, even when the worktree lives outside it.
# ~/projects/.ai-memory.toml
workspace = "oss"
project_strategy = "repo-root"
The same file holds [capture] rules with ignore_paths, and ai-memory install-hooks --apply --capture-mode allowlist turns the marker into an opt-in: a repository without one emits no events. Native hooks enforce both. The Docker wrapper’s shell hooks do not. Read the marker file reference.
Auto-scope modes
An MCP call without an explicit project resolves through a “current project” pointer. The mode decides who shares that pointer. The server logs the live mode at startup.
| Mode | When to use it |
|---|---|
per_actor | The default. Isolates parallel harnesses and separate people on one server. |
per_session | For session-aware clients that forward the hook session id on every MCP request. |
single | The behaviour before v1.39. One slot for the whole process, last write wins. Unsafe on a shared server. |
[auto_scope]
mode = "per_actor" # "per_actor" (default since v1.39) | "per_session" | "single"
session_ttl_secs = 3600 # TTL for per-key entries (default 1 h)
max_entries = 4096 # hard cap; oldest insertions evicted first
Read how auto-scope isolation works
SSO and OIDC
Each developer logs in once with an OIDC device flow against any standards-compliant issuer, such as Keycloak, Okta or Entra ID. The token then authenticates native lifecycle hooks and CLI commands when no static bearer token is set.
ai-memory auth login oidc-device \
--issuer "https://issuer.example.com/realms/team" \
--client-id "ai-memory-cli"
Offline, limits and upgrades
What a security review, a capacity plan and a maintenance window each need to know.
Air-gapped install
Binaries
Every release asset has a .sha256 file. Download on a connected machine, verify, carry it in. Releases carry checksums only, with no SLSA provenance or artifact attestation.
Source builds
SQLite is bundled and libgit2 is vendored, so the build needs a C toolchain and your crates mirror. cargo vendor works.
The embedding model
A default install fetches the model from Hugging Face on first start. For no outbound request at all, place model.safetensors, tokenizer.json and config.json in <data_dir>/models/all-MiniLM-L6-v2/ beforehand. Checksums are pinned in source.
The binary has no telemetry. The Docker wrapper checks Docker Hub for a newer image at most once every 24 hours, and AI_MEMORY_NO_VERSION_CHECK=1 turns that off. Updates in an air gap are manual, the same way as the install. Read the offline install page.
Capacity
Every write goes through one writer. The project measured where that stops, with cargo test -p ai-memory-store --test writer_throughput -- --ignored --nocapture.
| Concurrent writers | Throughput | Mean latency |
|---|---|---|
| 1 | 42/s | 23.9 ms |
| 8 | 295/s | 3.4 ms |
| 32 | 698/s | 1.43 ms |
| 128 | 700/s | 1.43 ms |
- The ceiling is about 700 writes per second, reached near 32 writers.
- The write queue is bounded at 1024. Past that, producers slow down and every write still lands.
- Shell hooks give up on the server after 200 ms and spool the event locally, so a slow server does not stall your agent.
AI_MEMORY_HOOK_RATE_PER_SECandAI_MEMORY_HOOK_RATE_BURSTadd an optional rate limit per actor and session. It is off by default.
Upgrading
ai-memory upgrade
- With the Docker wrapper, this verifies and replaces the wrapper, pulls the image and restages hook scripts. A server on another host is upgraded separately.
- Schema and wiki migrations run on start. They are forward-only: an older binary refuses to open a migrated directory, so run
ai-memory backupfirst if you might roll back.
Questions and answers
Does ai-memory need TLS?
Not on a single-user laptop bound to 127.0.0.1, and not over stdio. Add a TLS proxy when accounts exist, when the server is bound beyond loopback, when you open the web UI from another machine, or when it is reachable beyond the LAN. ai-memory does not terminate TLS itself.
What do I need to back up?
Run ai-memory backup, which is safe while the server runs. The tarball holds the wiki tree, a consistent SQLite snapshot and config.toml. The wiki is the source of truth. Pages, links and search can be rebuilt from it with ai-memory reindex, but sessions, handoffs, users and keys exist only in the database.
Does ai-memory support SSO?
It supports OIDC device auth for lifecycle hooks and CLI commands against any standards-compliant issuer. The server does not validate OIDC tokens itself, so putting the server API behind your identity provider requires an OIDC-aware gateway in front of it.
Can two servers share one data directory?
No. Run one server per data directory. Since 2.0 the server takes an exclusive lock on .serve.lock and a second server refuses to start.
Running it somewhere unusual?
Open an issue and describe your setup. The source, the docs and the tracker are all public.