Skip to Content
GuidesDeployment

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_memories

Or programmatically:

from prme import MemoryClient with MemoryClient("./my_memories") as client: # Directory created automatically pass

Backup

Copy the entire directory while the engine is closed:

cp -r ./my_memories ./my_memories_backup

A 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.prme

Or export as JSON:

prme export ./my_memories/memory.duckdb > backup.json

PostgreSQL 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/prme

Or 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-passphrase

Or with a pre-generated Fernet key:

export PRME_ENCRYPTION_KEY=ZmVybmV0LWtleS1oZXJlLTQ0LWNoYXJzLWJhc2U2NA==

How It Works

For a directory pack:

  1. On startup (MemoryEngine.create()): Decrypts .enc files in the memory directory
  2. During operation: Data is unencrypted in memory and on disk
  3. 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 pack

For 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_encryption and keeps those files in a memories.prme.tmp directory 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 EncryptionError and 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 fastembed provider runs locally. For production, consider openai embeddings for better quality, or keep fastembed for zero-dependency local operation.
  • Organizer: Opportunistic maintenance runs during retrieve/ingest with a 200ms budget. Increase PRME_ORGANIZER__OPPORTUNISTIC_BUDGET_MS for larger memory stores, or disable with PRME_ORGANIZER__OPPORTUNISTIC_ENABLED=false and run organize() on a schedule.

Monitoring

  • Use prme doctor ./memories to check health
  • Use prme stats ./memories/memory.duckdb for metrics
  • The HTTP API exposes /v1/health and /v1/stats endpoints
  • Watch materialization_debt — a growing debt means ingestion outpaces materialization

Scaling

ScaleRecommendation
< 10K nodesDuckDB is fine
10K - 100K nodesDuckDB with tuned organizer budgets
100K+ nodesPostgreSQL backend
Multi-processPostgreSQL backend

Security

  • Set PRME_ENCRYPTION_ENABLED=true for data at rest
  • The HTTP API binds to 127.0.0.1 and disables CORS by default. python -m prme.api refuses to start on a non-loopback address such as 0.0.0.0 unless PRME_API_USER_KEYS or PRME_API_API_KEY is set. See Running the Server for what counts as loopback and for the --allow-unauthenticated-external-bind override, which gives every caller operator access and is only for a server that nothing but an authenticating reverse proxy can reach. Pin PRME_API_CORS_ORIGINS if browser clients need access
  • If another ASGI server hosts the app (for example uvicorn prme.api.app:create_app --factory or 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:8000 only and requires PRME_MCP_USER_KEYS
  • The PostgreSQL in the repository’s docker-compose.yml is a local test database with fixed credentials; see Local test database versus a deployed database
  • Never commit .env files 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 prme

To 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.