v7 → v8 Migration Guide
AxonFlow's v8.0.0 identity-model rework is a terminology and enforcement cleanup, not a breaking API redesign. For most customers using the SDKs, plugins, or HTTP API normally, no application changes are required on the v8.0.0 upgrade. This guide explains what's changing conceptually, what stays the same on the wire, and the two situations that do break: direct SQL queries on a shared DB under the application role, and self-hosted forks calling the removed middleware.RLSMiddleware pool-scope helpers.
One-line summary: keep using
clientIdandclientSecret; the platform separates your customer organization (org_id) and your API credential (client_id) as distinct concepts internally;tenant_idJSON fields and theX-Tenant-IDheader keep working through v8 and are planned for removal in a future v10.
Which env vars do I need for the v8 upgrade?
The most common reader question. Use this table to figure out whether you need to do anything before deploying the v8.0.0 image.
| Deployment shape | AXONFLOW_DB_APP_ROLE_URL | AXONFLOW_DB_PLATFORM_ADMIN_URL |
|---|---|---|
| Community, single-tenant self-hosted (docker-compose, single-org, no cross-org workers) | Yes (or rely on DATABASE_URL fallback in dev) | No |
| Enterprise self-hosted with customer-portal | Yes | Yes |
| Enterprise self-hosted with multi-node enforcement | Yes | Yes |
| Enterprise self-hosted with AWS Marketplace metering | Yes | Yes |
| Single-tenant enterprise without any of the above | Yes | No |
Simple-path community upgrade (the common case)
If you run AxonFlow as a self-hosted single-tenant deployment (docker-compose or your own VPC, no customer-portal, no cross-org sweepers), the upgrade is three steps:
- Run
scripts/operators/provision-app-role.shonce before upgrade. It creates theaxonflow_app_rolePostgres role with the password you supply and verifies connectivity. - Set
AXONFLOW_DB_APP_ROLE_URLin your env (or your task definition / docker-compose / Kubernetes Secret) to the DSN authenticating asaxonflow_app_role. - Deploy the v8.0.0 image. Done. Single-tenant traffic is now FORCE-RLS-enforced; no cross-org workers fire.
You don't need AXONFLOW_DB_PLATFORM_ADMIN_URL unless you run one of:
- the customer-portal admin handlers (admin API key + customer management endpoints)
- multi-node enforcement (
ENABLE_NODE_MONITOR=true) - AWS Marketplace metering
- community-saas registration mode
- GDPR right-to-erasure (
/api/v1/tenant/{id}/delete-*) endpoints
If any of those apply, you also need to provision axonflow_platform_admin (BYPASSRLS) and set AXONFLOW_DB_PLATFORM_ADMIN_URL. The full multi-tenant operational recipe — role provisioning, DSN format, customer-portal admin DSN wiring, NodeMonitor + Marketplace env pairing — lives in the enterprise migration guide; for the community single-tenant case nothing beyond step 1-3 above is required.
What's changed conceptually
Pre-v8.0.0 (the v7.x train), AxonFlow used tenant_id for two different things:
- The customer organization that owns the data (the natural boundary for Row-Level Security).
- The API credential / app identity that authenticated a given request.
That overload caused real bugs: Community-SaaS customers all shared org_id="community-saas" while their actual customer identity lived in tenant_id, and several audit/policy write paths left org_id empty. The v8 identity model separates the two concepts and adds a third for completeness:
| Identifier | What it is | RLS-relevant? |
|---|---|---|
org_id | Customer organization. Tenant-isolation boundary. | Yes — Row-Level Security uses this in v8.0.0. |
client_id | API credential / app identity inside an org. (One org can have many credentials: prod, staging, per-service.) | No — credentials inside one org can read each other's data subject to policy. |
| deployment license identity | The AxonFlow installation that booted this agent. Validated at startup against the license. | No — never written to customer-data rows. |
A worked example:
- An enterprise customer Acme Corp runs AxonFlow in their own VPC. Their
org_idisacme-corp. They issue three Basic Auth credentials —acme-prod-api,acme-staging-api,acme-batch-jobs. Each one is a distinctclient_idinside the sameorg_id. Their license identifies the deployment — the value they used at deployment time (often the same as theirorg_id, but conceptually a separate field). - A Community-SaaS user cs_abc123 registered through
try.getaxonflow.com. Theirorg_idiscs_abc123and theirclient_idis alsocs_abc123(one customer, one credential — same value for both). The deployment identity isaxonflow-community-saas(AxonFlow's installation, not theirs). - A self-hosted Community user who never set
ORG_IDruns withorg_id = local-dev-org(the default — stable across versions forever). Theirclient_idis their Basic Auth username.
What stays the same on the wire
The compatibility promise for v8.0.0:
- JSON field names are unchanged. The registration response from
/api/v1/registerstill returns"tenant_id". The Plugin Pro Stripe checkout still has atenant_idcustom field. Same field, same value — the v8 model just calls itclient_idsemantically. X-Tenant-IDHTTP header is accepted. The agent and orchestrator both honour it as a deprecated alias forX-Client-ID. The SDK and proxy emit bothX-Client-IDandX-Tenant-IDoutbound during the v8 compatibility window.- Basic Auth credentials are unchanged. Existing
clientId/clientSecretpairs continue to work. The SDK already uses the v8-correct field name. - Plugin Pro license tokens are unchanged. The
tenant_idfield in the encoded token payload is still the Basic-Auth-derived value; v8 just calls thatclient_idin code. - License payload — V2 keeps working; V3 adds
deployment_idalongside. The v8 license payload format (V3) adds a newdeployment_idJSON field carrying the deployment/licensee identity, separate from the customer-roworg_id. V3 payloads also retainorg_idso v7 (V2-only) readers continue to validate the same signed payload. The agent picksdeployment_idwhen present and falls back toorg_idotherwise. Customers do not need to regenerate licenses; existing V2 licenses keep working through v8 and beyond.
What you should know (but not necessarily do)
A few v8 internals worth understanding, even if you don't need to act on them:
- AxonFlow-operated deployments are now
axonflow--prefixed. Internal deployments such ascommunity-saasandproduction-ushave moved toaxonflow-community-saas,axonflow-production-us, etc. This affects only AxonFlow's own deployments — customerorg_idvalues stay free-form (and should not start withaxonflow-unless you're AxonFlow). - The
axonflow-org_id prefix is now load-bearing. The telemetry classifier matches anyorg_idstarting withaxonflow-as the primary signal for "this row came from AxonFlow's own infrastructure." If you happen to use anorg_idmatching that prefix in your own deployment, change it. - Row-Level Security is enforced under
axonflow_app_role. Pre-v8.0.0, the agent connected as the table owner, which bypasses RLS even whenFORCE ROW LEVEL SECURITYis enabled. v8.0.0 defaultsAXONFLOW_DB_USE_APP_ROLE=true, so the agent connects asaxonflow_app_role(aNOBYPASSRLSrole) and FORCE RLS policies actually filter every SELECT, INSERT, UPDATE, DELETE. Theaxonflow_platform_adminrole (BYPASSRLS) handles legitimate cross-org access. See "Things that break" below for details.
What you need to do — usually nothing
Per deployment mode:
Self-hosted Community (Docker / docker compose up)
Required: the three-step "Simple-path community upgrade" above (provision the app role; set AXONFLOW_DB_APP_ROLE_URL; deploy v8.0.0).
- Your
ORG_IDenv var (or its defaultlocal-dev-orgif unset) keeps working unchanged. - Existing Basic Auth credentials keep working.
- Existing data remains queryable.
- Old
X-Tenant-IDheaders keep working as deprecated aliases. - The bundled Postgres container is provisioned with both
axonflow_app_roleandaxonflow_platform_adminroles by the agent's first-boot migration runner — theprovision-app-role.shscript is for self-managed Postgres (RDS, Aurora, on-prem), not for the bundled docker-compose Postgres.
Optional (recommended): start using client_id terminology in your own application code instead of tenant_id. The SDK already does this; if you've extended it with custom code, the v8 model gives you a cleaner mental model.
In-VPC Enterprise
Required: the enterprise migration recipe. The summary:
- Take a fresh database snapshot. The rollback contract is snapshot restore + image revert.
- Run the v8.0.0 self-hosted preflight script:
scripts/deployment/v9_self_hosted_preflight.sh(filename retains thev9_prefix from when this work was planned for the v9.0.0 cut; the script ships with the v8.0.0 platform). Exits 1 on any FAIL. - Provision both
axonflow_app_role(NOBYPASSRLS) andaxonflow_platform_admin(BYPASSRLS). - Populate
AXONFLOW_DB_APP_ROLE_URLandAXONFLOW_DB_PLATFORM_ADMIN_URLsecrets, then expose them as env vars on the agent + customer-portal + orchestrator task definitions.
If you would prefer to upgrade the platform image to v8.0.0 without immediately flipping the runtime role, set AXONFLOW_DB_USE_APP_ROLE=false explicitly. This preserves the legacy semantics — the agent continues to connect as the table owner — while letting you stage the role provisioning over a longer rollout window. Note: under this opt-out, the cross-org workers (Marketplace metering, NodeMonitor, CSAAS-SWEEP, CSAAS-RECOVERY, CSAAS-DELETE, customer-portal admin) continue to operate against the master role and FORCE RLS remains decorative on the protected tables. Once you finish role provisioning, remove the env override.
Recommended on v8.0.0: when you next issue API credentials through your existing customer portal or admin workflow, treat each as a distinct client_id inside the same org_id rather than reusing one credential across environments. For example: org_id=acme-corp with client_id=acme-prod-api and a separate client_id=acme-staging-api. This unlocks per-credential audit attribution and per-credential rate limits. No action is required at upgrade time — existing credentials keep working unchanged.
The detailed multi-tenant operational setup (NodeMonitor + Marketplace + customer-portal admin DSN format + per-feature env var combinations + DSN secrets-manager wiring + CFN parameter recipes + RDS snapshot/restore gotchas) lives in the enterprise migration guide.
Community SaaS (using try.getaxonflow.com)
Required: nothing.
- Your existing
cs_*credential keeps working. - Your existing Plugin Pro entitlement keeps working.
- The registration response still returns
tenant_idas the JSON field name. In v8 terminology this value is both yourorg_idand yourclient_id(one customer, one credential).
No re-registration needed.
Things that break on the v8.0.0 upgrade
Change 1 — Direct SQL under the application role
If you maintain a customer-portal or analytics dashboard that issues SQL directly against the AxonFlow application database (rather than calling the AxonFlow HTTP API), and that connection is using the application role (axonflow_app_role, not the master role), then those queries will return zero rows unless they set the app.current_org_id session variable first:
-- Before SELECT, set the current organization context.
-- The value is the same as your agent's ORG_ID env var (self-hosted / in-VPC)
-- or your cs_<uuid> credential (Community-SaaS). Connect as the master role
-- or axonflow_platform_admin once to enumerate, e.g.:
-- SELECT DISTINCT org_id FROM organizations;
SELECT set_config('app.current_org_id', 'your-customer-org-id', false);
-- Then your SELECT respects RLS and returns rows for that org only:
SELECT * FROM audit_logs WHERE created_at > now() - interval '1 hour';
This is intentional — it's the security mechanism v8.0.0 unlocks. Operators with legitimate cross-org needs (analytics rollups, sweep workers, support tooling) should connect using the axonflow_platform_admin role, which has BYPASSRLS and is the explicit cross-org channel.
This only affects deployments where:
- you have direct DB access (typically self-hosted or your own In-VPC deployment), and
- your tooling bypasses the AxonFlow HTTP API.
Customers using only the SDKs / plugins / HTTP API are not affected.
Change 2 — Customized handlers must wrap writes to RLS-enabled tables (self-hosted source forks)
AXONFLOW_DB_USE_APP_ROLE=true on a forkIf you maintain a fork of axonflow-enterprise with customized handlers — custom connectors, in-tree auth shims, extensions to the customer-portal or agent that issue their own INSERT / UPDATE / DELETE against the AxonFlow application database — those customized writes must be audited before flipping AXONFLOW_DB_USE_APP_ROLE=true.
Why: v8.0.0 makes FORCE Row-Level Security effective on a base set of customer-data tables (the migrations enable RLS on tables that carry org_id — audit_logs, mcp_query_audits, dynamic_policies, policy_overrides, organizations, tenants, connector_configs, usage_events, and others). Under the pre-v8.0.0 master-role connection, FORCE RLS was decorative — the master role bypassed every policy via table ownership. Under axonflow_app_role (NOBYPASSRLS), the same writes are gated by the policy's WITH CHECK predicate (typically org_id = current_setting('app.current_org_id', true)). A customized write that runs outside an org-scoped transaction fails with pq: new row violates row-level security policy. A customized DELETE that runs outside an org-scoped transaction silently filters to zero rows without error — DELETE evaluates the policy's USING predicate against rows already filtered by app.current_org_id = NULL, so no rows match and no error is raised.
What to do: for each customized INSERT / UPDATE / DELETE against an RLS-enabled table, confirm the call uses one of these three patterns:
| Pattern | When to use | API |
|---|---|---|
| Per-request org-scoped transaction | Most application writes — the request carries an org_id in its auth context | WithOrgScope(ctx, db, orgID, func(tx *sql.Tx) error { … }) (agent) or withRequestOrgScope(r, h.db, fn) (customer-portal) |
| SECURITY DEFINER helper | Pre-org-context writes (registration, signup — the org is being minted in the same request) | Call a SECURITY DEFINER function created by a migration. v8.0.0 ships auth_lookup_api_key() and auth_touch_api_key(); add your own via a migration if your customization needs one. |
| Admin (BYPASSRLS) pool | Cross-org workers (sweeps, mirrors, aggregators, tenant-delete) that genuinely iterate across orgs | Open the pool via OpenPlatformAdminConnection. Requires AXONFLOW_DB_PLATFORM_ADMIN_URL to be set. |
Refuse-to-boot guard: the v8.0.0 agent, orchestrator, and customer-portal binaries refuse to boot when AXONFLOW_DB_USE_APP_ROLE=true (the default — also active when the env is unset) and a cross-org worker is enabled (Marketplace metering, NodeMonitor, community-saas sweep / recovery / tenant-delete, or customer-portal admin handlers) and AXONFLOW_DB_PLATFORM_ADMIN_URL is not set. Community single-tenant deployments running none of the listed cross-org workers do not need AXONFLOW_DB_PLATFORM_ADMIN_URL — the guard is a no-op in that shape. When the guard does fire, the binary exits with a FATAL log line naming both env vars. The previous behavior was a silent fallback to the request-traffic pool, which under FORCE RLS caused those cross-org workers to quietly return zero rows. The FATAL log prefix names which worker triggered the guard:
[Marketplace]— AWS Marketplace metering[NodeMonitor]— multi-node enforcement[CSAAS-SWEEP]— community-saas registration cleanup[CSAAS-RECOVERY]— community-saas recovery handler[CSAAS-DELETE]— community-saas tenant-delete cross-org handler[customer-portal]— admin API handlers
Staged rollout: flip AXONFLOW_DB_USE_APP_ROLE=true on a staging stack with production-shaped traffic for at least one full diurnal cycle before flipping in production. Watch agent + orchestrator logs for pq: new row violates row-level security policy — each such line names the table the customized handler tried to write outside an org-scoped transaction. If you surface violations, set AXONFLOW_DB_USE_APP_ROLE=false and re-run under the legacy posture while you fix the handler. The env override is a temporary phased-rollout lever, not a permanent posture.
Customers using only the SDKs / plugins / HTTP API — and self-hosted deployments running stock v8.0.0 without customization — are not affected by this. The shipped codebase wraps every hot-path write correctly.
Change 3 — middleware.RLSMiddleware and 3 sibling helpers removed (self-hosted source forks)
Four pool-scope GUC functions in ee/platform/customer-portal/middleware/rls.go — RLSMiddleware, SetRLSContextForSession, ResetRLSContext, WithRLS — were removed in v8.0.0. They issued SELECT set_org_id($1) against *sql.DB (the connection pool), which is unsafe under FORCE RLS because the GUC landed on one connection while the next handler statement might execute on a different one.
If you maintain a self-hosted source fork that calls any of these directly, migrate to api.withRequestOrgScope(r, h.db, fn). The replacement is a request-scoped helper that opens a single *sql.Conn from the pool, sets app.current_org_id on that connection, runs the handler closure, and releases the connection.
Diagnostic helpers in middleware/rls.go (GetCurrentOrgID, VerifyRLSActive, GetRLSStats, RLSHealthCheck) are retained.
This affects self-hosted source forks only. Customers using only the SDKs / plugins / HTTP API are not affected.
Change 4 — Cross-org workers route through axonflow_platform_admin (community-saas deployments)
The community-saas-mode cross-org workers — sweep, recovery, tenant-delete (right-to-erasure), node monitor, customer-portal admin handlers — now require AXONFLOW_DB_PLATFORM_ADMIN_URL set on the relevant binary's task definition. Under v8.0.0, these workers explicitly open a separate *sql.DB authenticated as axonflow_platform_admin via OpenPlatformAdminConnection(). Self-hosted deployments running community-saas mode must populate the admin DSN secret before the v8.0.0 binary will boot.
The refuse-to-boot guard catches misconfiguration loudly. The previous behavior was a silent fallback to the request-traffic pool, which under FORCE RLS caused cross-org operations to silently return zero rows — including a particularly bad failure mode for the tenant-delete handler, where the cross-org DELETE filtered to zero rows under app_role, returned HTTP 200 to the caller, and wrote a GDPR Article-17 deletion log row asserting deletion completed while the registration + usage_events rows remained on disk. Routing through the admin pool closes that.
Note — additional v8.0.0 guarantees in this area:
- Agent
agent_heartbeatsUPSERT under FORCE RLS — the agent'ssendHeartbeatpath wraps the UPSERT in an org-scoped transaction so the heartbeat write succeeds against theaxonflow_app_roleconnection without falling back to the admin pool.- AST audit walker for write-path coverage — a build-time guard preventing customized writes from bypassing the three wrap patterns is in place; CI fails on any unwrapped
INSERT/UPDATE/DELETEinto an RLS-gated table. Stock v8.0.0 ships with the audit clean; self-hosted source forks that maintain customized handlers inherit the same CI guard when they pick up the v8.0.0 codebase.
Field-by-field cheat sheet
For programmatic consumers:
| Where you see it | Pre-v8 name | v8 conceptual name | On-wire field name |
|---|---|---|---|
/api/v1/register response | tenant_id | client_id (and org_id for Community-SaaS, where they share a value) | unchanged: tenant_id |
| Stripe Plugin Pro checkout custom field | tenant_id | client_id | unchanged: tenant_id |
| HTTP request header | X-Tenant-ID | X-Client-ID | both accepted; X-Client-ID is canonical |
| SDK config (TS/Py/Go/Java) | clientId | client_id | unchanged: clientId (was already v8-correct) |
| Basic Auth username | the cs_* / your credential ID | client_id | unchanged: same string |
| License payload field | V2 used org_id only | V3 adds deployment_id (canonical) | V3 mints both deployment_id and org_id; agent prefers deployment_id. V2-only payloads still validate. |
| Audit row column | varies | org_id (customer) + client_id (credential) | new client_id column added; legacy tenant_id remains as deprecated alias |
Header migration timeline
| Version | X-Client-ID | X-Tenant-ID | Agent → orchestrator forwarding |
|---|---|---|---|
| Pre-v7.9 | not recognized | used internally for cross-process forwarding; tenant derived from Basic Auth on external requests | forwards X-Tenant-ID only |
| v7.9.x | recognized inbound + forwarded outbound | accepted as alias | forwards both X-Client-ID and X-Tenant-ID |
| v8.0.0 (current release) | canonical | accepted as deprecated alias | forwards both, with X-Client-ID as primary |
| v10 (planned, no date) | required | rejected | forwards X-Client-ID only |
You may start using X-Client-ID on inbound requests today. You do not need to switch in v8.0.0 — v10 will be the version where X-Tenant-ID stops being accepted. Most callers never set either header explicitly; the SDKs and platform handle this internally.
License payload — V2 and V3
The V2 license payload uses an org_id JSON field for the deployment identity. The naming was a source of confusion because it's distinct from the customer-row org_id. V3 shipped in the v8.x train and adds a new deployment_id field carrying the same semantics with a clearer name.
What v8.0.0 code does:
- Mints both
deployment_idandorg_idon the V3 payload, with the same value. V2-only readers continue to validate signed payloads; V3-aware readers preferdeployment_id. - Exposes
LicenseDeploymentID()accessor on the parsed license — call this in new code rather than reading.OrgIDdirectly. - Validates V2 payloads unchanged. Customers do not need to regenerate licenses for v8.0.0.
If you parse license JSON yourself and need to know "which field do I read?", read deployment_id first and fall back to org_id. Both will be the same value on a freshly-minted V3 license.
The org_id field on the license payload remains accepted through v8 as a back-compat alias and is planned for removal in a future v10 cut.
Smoke tests for the v8 identity plumbing
These tests confirm your callers are compatible with the v8 identity model. Run them against any v8.0.0 endpoint to confirm nothing regressed after the upgrade:
# 1. Confirm Basic Auth works (the most common breakage shape if anything regresses)
curl -X POST https://YOUR_ENDPOINT/api/v1/request \
-H "Authorization: Basic $(echo -n 'your-client-id:your-client-secret' | base64)" \
-H "Content-Type: application/json" \
-d '{"query": "ping", "request_type": "chat"}'
# 2. Confirm legacy X-Tenant-ID alias still resolves to your credential
curl -X POST https://YOUR_ENDPOINT/api/v1/request \
-H "Authorization: Basic $(echo -n 'your-client-id:your-client-secret' | base64)" \
-H "X-Tenant-ID: your-client-id" \
-H "Content-Type: application/json" \
-d '{"query": "ping", "request_type": "chat"}'
# 3. Confirm the new X-Client-ID header is accepted
curl -X POST https://YOUR_ENDPOINT/api/v1/request \
-H "Authorization: Basic $(echo -n 'your-client-id:your-client-secret' | base64)" \
-H "X-Client-ID: your-client-id" \
-H "Content-Type: application/json" \
-d '{"query": "ping", "request_type": "chat"}'
All three should return identical responses. The auth-derived identity always wins over headers — if header and Basic Auth credentials disagree, the agent uses the credentials.
Where each concept lives in the docs
- SDK Authentication — the
clientIdyou configure - Auth and Header Matrix — the wire-level identity headers, including the Identity Primer
- License Management — the deployment license identity (the third identifier)
- Community SaaS — where
cs_*values come from and how they map toorg_id/client_id - Plugin Pro — how the
tenant_idStripe field maps to your credential
Getting help
If a specific call started failing after a v8.0.0 upgrade, the most useful information for support:
- The exact endpoint (
/api/v1/request,/api/v1/register, etc.). - The Basic Auth
clientId(first 8 chars only — never share the secret). - Any
X-*-IDheaders you set (verbatim). - The HTTP status code and response body.
[email protected] — please mention "v8 migration" in the subject so it routes correctly.
