Storage Configuration
Configure where Reflexio stores data, including SQLite (default), Supabase, and PostgreSQL for production deployments.
Storage configuration determines where Reflexio stores data.
Default Storage (Open Source)
The open-source version uses SQLite by default — no storage configuration is needed. Reflexio automatically creates and manages a local SQLite database, so you can start using it immediately without any setup.
# No storage configuration required — SQLite is used automatically
client = ReflexioClient() # see Quickstart for connection options
config = client.get_config()
# config.storage_config is already set to SQLitecurl -X GET "${REFLEXIO_URL:-https://www.reflexio.ai}/api/get_config" \
-H "User-Agent: my-agent-reflexio" \
-H "Authorization: Bearer $REFLEXIO_API_KEY"Supabase Storage
Hosted Enterprise
Hosted Enterprise uses Supabase with automatic provisioning and managed infrastructure. Hosted accounts start with managed storage and can switch to their own Supabase project from Settings.
For production deployments that need managed cloud storage, you can configure Supabase manually:
from reflexio.models.config_schema import StorageConfigSupabase
storage = StorageConfigSupabase(
url="https://your-project.supabase.co",
key="your_service_role_key",
db_url="postgresql://reflexio_user:replace-me@host:5432/postgres",
# Optional reader credentials for search traffic. Search reads use these
# credentials; writes and consistency-sensitive reads still use url/key.
read_url="https://your-reader.supabase.co",
read_key="your_reader_service_role_key",
)
config.storage_config = storage
client.set_config(config)curl -X POST "${REFLEXIO_URL:-https://www.reflexio.ai}/api/set_config" \
-H "User-Agent: my-agent-reflexio" \
-H "Authorization: Bearer $REFLEXIO_API_KEY" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"...": "updated full config object"
}
JSON| Field | Type | Description |
|---|---|---|
url | str | Supabase project URL |
key | str | Supabase service-role key (needs write access) |
db_url | str | PostgreSQL connection string |
read_url | str | Optional. Supabase reader URL for search. Defaults to url. |
read_key | str | Optional. Reader key for search. Defaults to key. |
schema | str | Optional. Used by hosted Reflexio for platform-managed per-org schemas. Omit this for bring-your-own Supabase storage. |
Search uses reader credentials when provided, so search results can be bounded-stale on replicated infrastructure. Mutations and publish/generation consistency reads continue to use the writer credentials.
PostgreSQL Storage
Hosted Enterprise
Reflexio Enterprise can also store data in a customer-owned PostgreSQL database, such as AWS RDS, without running Supabase or PostgREST. Auth/login storage remains separate from data storage.
from reflexio.models.config_schema import StorageConfigPostgres
storage = StorageConfigPostgres(
db_url="postgresql://reflexio_user:replace-me@host:5432/database",
schema="public",
# Optional reader pool for search traffic
read_db_url="postgresql://reflexio_reader:replace-me@reader:5432/database",
read_pool_size=10,
)
config.storage_config = storage
client.set_config(config)curl -X POST "${REFLEXIO_URL:-https://www.reflexio.ai}/api/set_config" \
-H "User-Agent: my-agent-reflexio" \
-H "Authorization: Bearer $REFLEXIO_API_KEY" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"...": "updated full config object"
}
JSON| Field | Type | Description |
|---|---|---|
db_url | str | PostgreSQL connection string |
schema | str | Optional. Target schema for Reflexio data. Defaults to public. |
pool_size | int | Optional. Maximum direct SQL connections per target/policy/process. Defaults to 10. |
pool_acquire_timeout | float | Optional. Seconds a query waits for a free pooled connection before failing. Defaults to 30.0. |
read_db_url | str | Optional. Reader PostgreSQL connection string for search. Defaults to db_url. |
read_pool_size | int | Optional. Maximum reader connections per process. Defaults to pool_size. |
read_pool_acquire_timeout | float | Optional. Seconds search waits for a free reader connection. Defaults to pool_acquire_timeout. |
PostgreSQL storage requires PostgreSQL 15+ with pgvector available.
Connections are shared across work using the same database credentials and pool
policy in one process. Organizations sharing that target must use compatible pool
settings; conflicting settings are rejected. When concurrency exceeds pool_size,
additional queries wait up to pool_acquire_timeout before failing. Reader and
privileged-credential pools have separate capacity. Budget all pools across every
server process, including overlap during deployment, against the database limit.
In self-host deployments, pool settings can be set without editing config via
REFLEXIO_POSTGRES_POOL_SIZE, REFLEXIO_POSTGRES_POOL_ACQUIRE_TIMEOUT,
REFLEXIO_POSTGRES_READ_DB_URL, REFLEXIO_POSTGRES_READ_POOL_SIZE, and
REFLEXIO_POSTGRES_READ_POOL_ACQUIRE_TIMEOUT.
Search/read timeout knobs are also available for production deployments:
REFLEXIO_SUPABASE_HTTP_TIMEOUT_SECONDS for Supabase/PostgREST and
REFLEXIO_POSTGRES_STATEMENT_TIMEOUT_MS for native Postgres.
For enterprise native PostgreSQL and Supabase SQL pools,
REFLEXIO_DB_LEASE_TIMEOUT_SECONDS adds a separate 120-second total connection
lease limit. It starts after checkout and includes all statements, time between
statements, commit, rollback, and pool cleanup. It interrupts unresponsive database
socket operations even when the peer still acknowledges TCP traffic. It is not an
HTTP request timeout and does not interrupt arbitrary application computation.
The database lease deadline also protects the authentication PostgreSQL pool and background workers' dedicated transactions. Persistent maintenance sessions apply it per database operation or batch, so idle time does not release healthy advisory locks. Daemon coordination uses a fixed 15-second client deadline for its short database interactions. Expiry discards the connection without replaying writes; a commit whose response was lost may already have succeeded. Fleet schema migrations use a separate 30-minute per-operation budget, accommodating existing ten-minute index builds without imposing a time limit on the whole migration sweep.
Set a larger finite positive value for intentional long transactions; increasing only the statement timeout does not increase the lease limit. Unset or empty uses 120 seconds; zero, negative, NaN, and infinity are rejected. Expired connections are discarded. A timeout can leave a write's outcome unknown (the server may have committed before its response was lost), so reconcile the result or use an idempotency key before retrying a write. The pool does not replay it automatically.
Config Encryption
Hosted Enterprise
Enterprise can encrypt stored organization configuration before it is written to
the configuration store. This protects persisted configuration_json rows, such
as stored storage credentials; it does not encrypt the Reflexio data tables
themselves. Configure FERNET_KEYS with a comma-separated key ring. The first
valid key encrypts new writes, and older keys are accepted for reads during
rotation. Leaving FERNET_KEYS empty is valid and stores configuration as
plaintext.
Generate a key with:
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"Keep Fernet keys in your secret manager or deployment environment; never commit
them. Set FERNET_REQUIRED=true only after valid keys are deployed and existing
configuration rows have been re-encrypted. Required mode fails closed: if no
valid Fernet key is available, Reflexio refuses to store plaintext
configuration. During key rotation, deploy the new key first in the
FERNET_KEYS list, re-encrypt existing rows, then remove retired keys after all
running instances can read the newly encrypted values.
Row Retention
Reflexio applies high-water row limits to data tables. When an eligible table
reaches its limit, the server deletes the oldest 20% of rows for that table by
its target-specific ordering column; not every target uses created_at.
The check runs on the background retention sweep, once per project on the lineage garbage-collection poll interval — not on each publish, which is where it used to run.
# Defaults to 250000 rows per target
REFLEXIO_ROW_LIMIT_INTERACTIONS=500000
REFLEXIO_ROW_LIMIT_PROFILES=250000
# Set a target to 0 to disable its cleanup
REFLEXIO_ROW_LIMIT_PLAYBOOK_OPTIMIZATION_EVENTS=0Set a cap to at least five days of ingest
The sweep deletes 20% of the current row count per tick, so a cap only settles
when limit >= 5 x daily_ingest. Above that the table converges; below it the
table keeps growing past its limit, because each tick removes less than a day
adds.
At the 250,000 default that threshold is 50,000 rows/day, which every target is
comfortably inside. It matters if you lower a cap to bound a hot table: setting
REFLEXIO_ROW_LIMIT_INTERACTIONS=10000 on a project ingesting more than 2,000
interactions a day will not hold that table at 10,000.
retention.cap.enforced reports rows_before on every deletion, so a table that
is climbing rather than settling is visible in that series.
Disabling cleanup
Set that target's REFLEXIO_ROW_LIMIT_* to 0, as above. That is the only
supported switch.
Earlier versions of this page described
REFLEXIO_RETENTION_CLEANUP_INTERVAL_SECONDS=0 as disabling the periodic sweep.
It never did: the code read <= 0 as no throttle, meaning the sweep ran on
every publish rather than not at all. That variable has been removed along
with the publish-path sweep, so no deployment loses a capability it had — the
documented behaviour was the opposite of the implemented one.
The legacy INTERACTION_CLEANUP_THRESHOLD variable still applies to
interactions when REFLEXIO_ROW_LIMIT_INTERACTIONS is unset.
The user_playbook_exposure_events target is fixed at 250,000 rows in code and
does not accept a REFLEXIO_ROW_LIMIT_USER_PLAYBOOK_EXPOSURE_EVENTS override.
Most retention targets are ordered by created_at. The exceptions are:
offline_tuner_reward_label rows use label_created_at, and
user_playbook_exposure_events rows use ingested_at. Exposure events also
have a 14-day minimum age floor, so row-count cleanup cannot delete them until
they are at least 14 days old.
Managed cloud: plan-based retention
On the managed cloud, the row limits above work differently for customer data:
- Raw events expire by plan. Interactions, requests, evaluation results, and exposure events older than your plan's retention window are archived and then deleted. The window is 30 days on Free and 90 days on Pro, and during a signup trial the trial plan's window applies. Deletion is rolled out gradually and is currently reported only. Nothing is deleted by age until it is switched on.
- Learned knowledge does not expire by age. Active profiles and playbooks are kept. Only retired (archived, merged, superseded or expired) rows are removed, after the lineage grace window.
- Customer-data row limits alert instead of deleting. Profiles, interactions, requests, playbooks, evaluation results and skills have a high safety limit. Reaching it alerts our team and deletes nothing.
Self-hosted deployments keep the row limits described above. Plan-based expiry is a managed-cloud feature.
Enterprise Self-Host Single Database
Hosted Enterprise
Enterprise self-host deployments can run with one customer-owned database for both login metadata and Reflexio data. Set DEPLOYMENT_MODE=self_host, choose REFLEXIO_STORAGE=supabase or REFLEXIO_STORAGE=postgres, and provide SELF_HOST_USERNAME / SELF_HOST_PASSWORD for the only login account.
For Supabase self-host, configure DATA_SUPABASE_URL, DATA_SUPABASE_KEY, and DATA_DB_URL. For vanilla Postgres self-host, configure DATA_DB_URL. Startup applies auth and data migrations to that same database and stores the generated configuration_json in public.organizations.
Usage-metering WAL volume (self-host)
Hosted Enterprise
REFLEXIO_SELF_HOST_METERING=required|exempt selects the self-host metering composition. Unset or blank defaults to required. Only a trusted deployment owner may deliberately select exempt for an intentionally unmetered installation; paid deployment tooling must not set it accidentally.
In required mode, usage is written to a small encrypted write-ahead log (WAL)
on disk rather than your database. Mount a persistent, writable volume at
REFLEXIO_USAGE_WAL_PATH (default ~/.reflexio/usage_wal) for restart-safe
metering. Boot fails when the resolved directory is not writable; an ephemeral
or default path logs a warning.
If the control plane temporarily cannot complete a usage-ingest cardinality
read, it returns HTTP 503 with Retry-After: 5. The shipper retains the batch
in its WAL and retries without double-counting acknowledged usage. Keep the WAL
volume intact while database availability recovers.
For multi-instance deployments, configure one shared persistent mount root and
let startup create derived per-instance directories at
<root>/<instance-id>. Distinct per-replica persistent volumes are an
alternative when every replica receives a stable, unique path. Reflexio's
generated, file-backed instance id is preferred; set REFLEXIO_INSTANCE_ID
only when the orchestrator guarantees uniqueness. If required-mode WAL files
become unreadable, recover the original preserved files rather than starting
from an empty directory.
In exempt mode, Reflexio does not construct or validate the usage WAL, and no
WAL path, persistence, or mount is required. Early startup may still normalize
or derive a configured path environment value before selecting the exempt
composition.
Activating a Self-Host Data Plane
Hosted Enterprise
When Reflexio provisions your self-host account, the admin console's Onboard self-host customer flow shows a one-time activation key. Configure your data plane with that key — the control-plane URL defaults to Reflexio cloud.
# The activation key shown once in the Reflexio admin portal.
BYOC_DEPLOYMENT_SECRET="rflx-dep-…"
# Optional — the Reflexio control-plane base URL. Defaults to the Reflexio cloud
# control plane (https://www.reflexio.ai); set it only to target a different one.
# CONTROL_PLANE_URL="https://www.reflexio.ai"CONTROL_PLANE_URL is the canonical setting. Older deployments that still set
CONTROL_PLANE_INGEST_URL continue to work as a fallback, but new deployments
should use CONTROL_PLANE_URL.
On startup the data plane calls POST /api/billing/byoc/activate with the
activation key in the X-Deployment-Secret header. The control plane confirms
the key→deployment binding and returns your deployment id, org id, and central
public key.
After activation, the data plane ships usage and pulls its entitlement lease
automatically. You do not need to set BYOC_DEPLOYMENT_ID; the activation
handshake supplies it.
Official self-host images already include Reflexio's public verification keys. With the default Reflexio control plane, installing or upgrading the image is sufficient: no public-key setting or enforcement flag is needed. The server verifies both licenses and centrally issued login tokens automatically. The existing staging control-plane URL selects staging keys separately.
If you operate a custom control plane, configure its public key with the advanced
CONTROL_PLANE_PUBLIC_KEY override. This replaces the bundled keys. Never copy
Reflexio's private signing key to a self-host server.
Missing or invalid signatures refuse gated enterprise features; core APIs remain
available. The former ENTERPRISE_REQUIRE_SIGNED_LEASE flag is ignored.
The activation key is your deployment's credential: keep it secret, and rotate it
from the admin portal if exposed. Credential enforcement for legacy BYOC
registration and ingest is controlled on the control plane with
BYOC_ENFORCE_REGISTER_CREDENTIAL and BYOC_ENFORCE_INGEST_CREDENTIAL. Data
plane operators normally do not set those flags; the data plane only needs the
per-deployment activation key shown above.