Deployment Guide
DuckDB (Default)
The default backend stores everything in a single directory:
my_memories/
memory.duckdb # Events + graph (DuckDB)
vectors.usearch # HNSW vector index (usearch)
lexical_index/ # Full-text search (Tantivy)This directory is portable — you can copy, backup, or encrypt it as a single unit.
A pack can instead be one file, as an opt-in: a single-file pack (memories.prme)
is one DuckDB database that holds the same data plus the vector snapshot and a
pack manifest, and rebuilds its lexical index in memory on open. Give
MemoryClient or prme init a path ending in .prme, or set PRME_PACK_PATH.
prme migrate ./my_memories ./memories.prme copies an existing directory pack
into one. See single-file packs.
Initialization
prme init ./my_memoriesOr programmatically:
from prme import MemoryClient
with MemoryClient("./my_memories") as client:
# Directory created automatically
passBackup
Copy the entire directory while the engine is closed:
cp -r ./my_memories ./my_memories_backupA single-file pack is copied the same way, as one file. While its engine is
open, DuckDB keeps recent changes in memories.prme.wal beside it, so copy the
file after the engine closes:
cp ./memories.prme ./memories_backup.prmeOr export as JSON:
prme export ./my_memories/memory.duckdb > backup.jsonPostgreSQL Backend
For production deployments with concurrent access, use PostgreSQL with pgvector.
Prerequisites
pip install prme[postgres]PostgreSQL must have the pgvector extension installed:
CREATE EXTENSION IF NOT EXISTS vector;Configuration
export PRME_DATABASE_URL=postgresql://user:pass@localhost:5432/prmeOr programmatically:
from prme.config import PRMEConfig
config = PRMEConfig(database_url="postgresql://user:pass@localhost:5432/prme")When database_url is set, PRME uses PostgreSQL for events, graph, and vectors (via pgvector). Full-text search uses PostgreSQL’s built-in capabilities.
Local test database versus a deployed database
The repository’s docker-compose.yml starts PostgreSQL with pgvector for local development and CI only. It uses the fixed prme_test user and password and publishes port 5432 on 127.0.0.1, so other machines cannot reach it. Do not change that mapping to a network address or reuse the test credentials.
A PostgreSQL that other machines can reach needs its own role with a strong, unique password, network rules that admit only the hosts running PRME, and TLS where the connection crosses a network. Put those credentials in PRME_DATABASE_URL from a secret store or an untracked .env file.
Schema
Tables are created automatically on first connection. The PostgreSQL backend provides:
- Connection pooling via asyncpg
- Native concurrent write support (no write queue needed)
- pgvector for vector similarity search
- PostgreSQL full-text search
Encryption at Rest
PRME can encrypt the memory pack at rest. A directory pack uses Fernet (AES-128-CBC + HMAC-SHA256) with PBKDF2 key derivation, as described below. A single-file pack uses DuckDB’s native encryption instead; see Single-file packs.
Enabling Encryption
export PRME_ENCRYPTION_ENABLED=true
export PRME_ENCRYPTION_KEY=my-secret-passphraseOr with a pre-generated Fernet key:
export PRME_ENCRYPTION_KEY=ZmVybmV0LWtleS1oZXJlLTQ0LWNoYXJzLWJhc2U2NA==How It Works
For a directory pack:
- On startup (
MemoryEngine.create()): Decrypts.encfiles in the memory directory - During operation: Data is unencrypted in memory and on disk
- On shutdown (
engine.close()): Encrypts database, vector index, and lexical index files
Encrypted files have the .enc extension. A manifest.json records encryption metadata.
Key Derivation
If the encryption key is a passphrase (not a 44-char Fernet key), PRME derives the key for a directory pack using:
- Algorithm: PBKDF2-HMAC-SHA256
- Iterations: 600,000
- Salt: randomly generated, stored in the encrypted file header
Prefix the key with raw_key: to mark a raw Fernet key, or with passphrase: to derive a key even from a value that looks like a Fernet key.
Lock/Unlock
You can encrypt/decrypt a directory pack without closing the engine:
engine.lock() # Encrypt memory pack
engine.unlock() # Decrypt memory packFor a single-file pack, both raise RuntimeError: DuckDB keeps such a pack encrypted at all times.
Single-file packs
A single-file pack (PRME_PACK_PATH, or a path ending in .prme) is encrypted by DuckDB itself when PRME_ENCRYPTION_ENABLED and PRME_ENCRYPTION_KEY are set, so it is never in clear text on disk, even while the engine runs:
- DuckDB encrypts the file and its write-ahead log with AES-256-GCM. It does not encrypt its temporary files by default, even for an encrypted database, so PRME turns on DuckDB’s
temp_file_encryptionand keeps those files in amemories.prme.tmpdirectory beside the pack, which can appear while the pack is open - A passphrase is turned into the DuckDB key with PBKDF2-HMAC-SHA256 (600,000 iterations) and the fixed salt
prme-single-file-pack-v1, because a salt cannot be stored inside a file that needs the key to open. The same passphrase therefore gives the same key for every pack, so use a strong passphrase and do not reuse it across packs. A raw key is used as given - A missing key, a wrong key, or encryption configured for an unencrypted pack raises
EncryptionErrorand leaves the file unchanged - An encrypted pack can be open in only one engine per process
prme migrate ./my_memories ./memories.prme, with the key configured, copies a Fernet-encrypted directory pack into a natively encrypted single-file pack and leaves the directory in place, encrypted
Production Checklist
Performance
- DuckDB: Single-writer. Use the write queue (enabled by default). For multi-process, use PostgreSQL.
- Embeddings: The default
fastembedprovider runs locally. For production, consideropenaiembeddings for better quality, or keepfastembedfor zero-dependency local operation. - Organizer: Opportunistic maintenance runs during retrieve/ingest with a 200ms budget. Increase
PRME_ORGANIZER__OPPORTUNISTIC_BUDGET_MSfor larger memory stores, or disable withPRME_ORGANIZER__OPPORTUNISTIC_ENABLED=falseand runorganize()on a schedule.
Monitoring
- Use
prme doctor ./memoriesto check health - Use
prme stats ./memories/memory.duckdbfor metrics - The HTTP API exposes
/v1/healthand/v1/statsendpoints - Watch
materialization_debt— a growing debt means ingestion outpaces materialization
Scaling
| Scale | Recommendation |
|---|---|
| < 10K nodes | DuckDB is fine |
| 10K - 100K nodes | DuckDB with tuned organizer budgets |
| 100K+ nodes | PostgreSQL backend |
| Multi-process | PostgreSQL backend |
Security
- Set
PRME_ENCRYPTION_ENABLED=truefor data at rest - The HTTP API binds to
127.0.0.1and disables CORS by default.python -m prme.apirefuses to start on a non-loopback address such as0.0.0.0unlessPRME_API_USER_KEYSorPRME_API_API_KEYis set. See Running the Server for what counts as loopback and for the--allow-unauthenticated-external-bindoverride, which gives every caller operator access and is only for a server that nothing but an authenticating reverse proxy can reach. PinPRME_API_CORS_ORIGINSif browser clients need access - If another ASGI server hosts the app (for example
uvicorn prme.api.app:create_app --factoryor Gunicorn), that server picks the bind address and PRME cannot check it: configure API credentials, or keep it on loopback behind an authenticating proxy - Bearer keys travel in plain text over HTTP: terminate TLS in front of the API whenever requests cross a network
- The MCP server runs over stdio (no network exposure) by default. Its Streamable HTTP transport listens on
127.0.0.1:8000only and requiresPRME_MCP_USER_KEYS - The PostgreSQL in the repository’s
docker-compose.ymlis a local test database with fixed credentials; see Local test database versus a deployed database - Never commit
.envfiles with API keys to version control
Docker
Example Dockerfile:
FROM python:3.11-slim
RUN pip install prme[api]
ENV PRME_DB_PATH=/data/memory.duckdb
ENV PRME_VECTOR_PATH=/data/vectors.usearch
ENV PRME_LEXICAL_PATH=/data/lexical_index
VOLUME /data
EXPOSE 8000
# 0.0.0.0 is required inside the container. The server refuses to start
# there without API credentials, so pass them at `docker run` time.
CMD ["python", "-m", "prme.api", "--host", "0.0.0.0", "--port", "8000"]Keep the credentials out of the image and out of your shell history, for example in an untracked prme-api.env file. This is Docker env-file syntax, where the value takes no surrounding quotes, so do not source it from a shell:
# prme-api.env (never commit this file)
PRME_API_USER_KEYS={"alice": "<generated key>"}Generate each key, for instance with python -c "import secrets; print(secrets.token_urlsafe(32))".
docker build -t prme .
docker run -v ./memories:/data -p 8000:8000 --env-file prme-api.env prmeTo keep the API on the host machine only, publish it on loopback instead: -p 127.0.0.1:8000:8000. The server inside the container still binds 0.0.0.0, which other containers on the same Docker network and every local user of the host can reach, so it still needs credentials.
Source: documentation/deployment.md in PRME at 58caddb1.