The same system, on your own hardware
Self-hosting includes every layer, endpoint and governance control. You operate the service and its configuration, including the plan quotas enabled by the production Compose profile.
- MIT licence, full permissive grant
- Seven compose services
- Postgres 16 with pgvector
What the licence actually permits
Memory OS is released under the MIT licence — a full permissive grant. There is no Commons Clause, no Business Source Licence and no open-core carve-out where the useful half is withheld. You may run it commercially, modify it and deploy it for your own customers.
The source is not on a public host today. If you want to self-host, write to support@pcnaid.com and we will arrange access. Publishing it openly is a decision we have not yet taken, and we would rather say that than imply a download link exists.
What you take on
- A Postgres 16 instance with pgvector, and its backups
- A password-protected FalkorDB instance for the production graph backend
- TLS, DNS and whatever sits in front of Caddy
- Provider keys, if you want model-backed summarisation
- Monitoring, and someone who notices when it breaks
There is no software licence fee. The production Compose profile
sets QUOTA_ENFORCEMENT_ENABLED=true, so self-hosting
retains plan limits by default. A new Free organization has a
UTC calendar-month allowance of 1,000 creates through
POST /v1/memory and one new knowledge-base source
creation. These are operation counters, not retained inventory
limits; deleting or disabling a record does not refund usage.
Each tenant has its own organization and plan. Review those limits
and your retention configuration
before inviting users.
Seven services, one compose file
The production profile. Nothing here is a managed service you have to buy.
| Service | Image | What it does |
|---|---|---|
postgres | pgvector/pgvector:pg16 | The canonical store. Embeddings via pgvector, full-text via tsvector and GIN, tenant isolation via forced row-level security. |
falkordb | falkordb/falkordb:latest | Required by the shipped production profile for password-protected graph memory. The backend and worker enable the FalkorDB graph backend and depend on this service being healthy. NetworkX is a separate local configuration, not an automatic production fallback. |
migrate | application image | Applies migrations as the schema owner, then exits. Runs with a different Postgres role from the API. |
backend | application image | The FastAPI service and the operator dashboard at /ui/. |
worker | application image | Drains the transactional outbox: graph upserts, fact extraction, knowledge-base refreshes, tenant exports and email. |
backup | PostgreSQL 16 backup image | Daily custom-format dumps in a dedicated backup_data volume, isolated from the API and worker. Copy them to independent protected storage for off-host recovery. |
caddy | caddy:2 | The sole public ingress. Terminates TLS via ACME and sets HSTS, CSP, frame, referrer and content-type headers. |
The one thing people get wrong
Row-level security is the floor this whole system stands on, and a
Postgres superuser walks straight through it — even on tables
declared FORCE ROW LEVEL SECURITY. If the API connects
as a superuser, tenant isolation is decorative.
So production splits the roles. The migration role owns the schema
and is used only by the migrate service. The API and the worker's
event handlers use a runtime role that is
NOSUPERUSER NOBYPASSRLS and owns nothing. The worker also
needs a separate read-only BYPASSRLS control role, limited
to SELECT on outbox_events, to discover tenant
queues; provision it before setting
POSTGRES_WORKER_CONTROL_DSN.
Verify it rather than assume it. The deployment notes give the exact query, and are blunt about the result: treat anything other than the expected answer as a deployment-blocking security failure. Our own CI asserts the same property on every run.
SELECT current_user,
rolsuper,
rolbypassrls
FROM pg_roles
WHERE rolname = current_user;
-- the only acceptable answer
current_user | esapp_runtime
rolsuper | f
rolbypassrls | f
Before you call it production
14 checks covering database roles, secrets, authentication and a recovery plan you have actually rehearsed.
- Runtime role is NOSUPERUSER and NOBYPASSRLS, and is not the schema owner.
- POSTGRES_MIGRATION_DSN is set separately and used only by the migrate service.
- POSTGRES_WORKER_CONTROL_DSN uses a read-only BYPASSRLS role with SELECT only on outbox_events for tenant discovery.
- POSTGRES_BACKUP_DSN uses a separate read-only backup role with BYPASSRLS, so every tenant is included in the dump.
- FERNET_KEY is set before any provider credential is registered.
- The original FERNET_KEY and an independent copy of each required backup are kept securely outside the application volume.
- AUTH_ENABLED is true and AUTH_LEGACY_FALLBACK is false.
- REQUIRE_TENANT_ID is true, so a request without a tenant claim is refused.
- QUOTA_ENFORCEMENT_ENABLED is set according to whether you meter.
- ALLOW_SEMANTIC_FORGET remains false.
- SYSTEM_CONTROL_ENABLED remains false unless you deliberately want remote compose control.
- CORS_ORIGINS is an explicit allowlist, never a wildcard.
- ALERT_RECEIVER_TOKEN is set if you route alerts into the receiver.
- A restore has actually been rehearsed, and the restore time and data-loss window are written down.
The command-line modules live under backend/app/scripts/.
From backend/, with PostgreSQL 16 client tools installed,
use python -m app.scripts.backup --output-dir data/backups.
Export POSTGRES_DSN for the dedicated backup role in that
shell. The API's tenant-isolated runtime role cannot dump every tenant.
For a staging restore, set BACKUP_FILE to the dump path and
STAGING_DSN to the staging database, then run
python -m app.scripts.restore "$BACKUP_FILE" --dsn "$STAGING_DSN"
from backend/. This replaces matching objects in the target
database. Follow it with migrations, row-level-security tests and a
smoke query before any cutover.
The production backup image supplies matching PostgreSQL 16 client
tools. Its daily dumps are retained in
backup_data:/app/data/backups, isolated from the API and
worker. They are not automatically copied
off-host or encrypted. Protect an independent copy and the original
FERNET_KEY, which is needed to decrypt restored provider
credentials. The repository's RUNBOOK.md includes the
backup-role setup and container commands.
Want the source?
Write to us and we will arrange access. It is MIT licensed — there is no negotiation, just a link we have not made public yet.