Skip to main content

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 clientId and clientSecret; the platform separates your customer organization (org_id) and your API credential (client_id) as distinct concepts internally; tenant_id JSON fields and the X-Tenant-ID header 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 shapeAXONFLOW_DB_APP_ROLE_URLAXONFLOW_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-portalYesYes
Enterprise self-hosted with multi-node enforcementYesYes
Enterprise self-hosted with AWS Marketplace meteringYesYes
Single-tenant enterprise without any of the aboveYesNo

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:

  1. Run scripts/operators/provision-app-role.sh once before upgrade. It creates the axonflow_app_role Postgres role with the password you supply and verifies connectivity.
  2. Set AXONFLOW_DB_APP_ROLE_URL in your env (or your task definition / docker-compose / Kubernetes Secret) to the DSN authenticating as axonflow_app_role.
  3. 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:

  1. The customer organization that owns the data (the natural boundary for Row-Level Security).
  2. 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:

IdentifierWhat it isRLS-relevant?
org_idCustomer organization. Tenant-isolation boundary.Yes — Row-Level Security uses this in v8.0.0.
client_idAPI 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 identityThe 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_id is acme-corp. They issue three Basic Auth credentials — acme-prod-api, acme-staging-api, acme-batch-jobs. Each one is a distinct client_id inside the same org_id. Their license identifies the deployment — the value they used at deployment time (often the same as their org_id, but conceptually a separate field).
  • A Community-SaaS user cs_abc123 registered through try.getaxonflow.com. Their org_id is cs_abc123 and their client_id is also cs_abc123 (one customer, one credential — same value for both). The deployment identity is axonflow-community-saas (AxonFlow's installation, not theirs).
  • A self-hosted Community user who never set ORG_ID runs with org_id = local-dev-org (the default — stable across versions forever). Their client_id is 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/register still returns "tenant_id". The Plugin Pro Stripe checkout still has a tenant_id custom field. Same field, same value — the v8 model just calls it client_id semantically.
  • X-Tenant-ID HTTP header is accepted. The agent and orchestrator both honour it as a deprecated alias for X-Client-ID. The SDK and proxy emit both X-Client-ID and X-Tenant-ID outbound during the v8 compatibility window.
  • Basic Auth credentials are unchanged. Existing clientId / clientSecret pairs continue to work. The SDK already uses the v8-correct field name.
  • Plugin Pro license tokens are unchanged. The tenant_id field in the encoded token payload is still the Basic-Auth-derived value; v8 just calls that client_id in code.
  • License payload — V2 keeps working; V3 adds deployment_id alongside. The v8 license payload format (V3) adds a new deployment_id JSON field carrying the deployment/licensee identity, separate from the customer-row org_id. V3 payloads also retain org_id so v7 (V2-only) readers continue to validate the same signed payload. The agent picks deployment_id when present and falls back to org_id otherwise. 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:

  1. AxonFlow-operated deployments are now axonflow--prefixed. Internal deployments such as community-saas and production-us have moved to axonflow-community-saas, axonflow-production-us, etc. This affects only AxonFlow's own deployments — customer org_id values stay free-form (and should not start with axonflow- unless you're AxonFlow).
  2. The axonflow- org_id prefix is now load-bearing. The telemetry classifier matches any org_id starting with axonflow- as the primary signal for "this row came from AxonFlow's own infrastructure." If you happen to use an org_id matching that prefix in your own deployment, change it.
  3. Row-Level Security is enforced under axonflow_app_role. Pre-v8.0.0, the agent connected as the table owner, which bypasses RLS even when FORCE ROW LEVEL SECURITY is enabled. v8.0.0 defaults AXONFLOW_DB_USE_APP_ROLE=true, so the agent connects as axonflow_app_role (a NOBYPASSRLS role) and FORCE RLS policies actually filter every SELECT, INSERT, UPDATE, DELETE. The axonflow_platform_admin role (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_ID env var (or its default local-dev-org if unset) keeps working unchanged.
  • Existing Basic Auth credentials keep working.
  • Existing data remains queryable.
  • Old X-Tenant-ID headers keep working as deprecated aliases.
  • The bundled Postgres container is provisioned with both axonflow_app_role and axonflow_platform_admin roles by the agent's first-boot migration runner — the provision-app-role.sh script 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:

  1. Take a fresh database snapshot. The rollback contract is snapshot restore + image revert.
  2. Run the v8.0.0 self-hosted preflight script: scripts/deployment/v9_self_hosted_preflight.sh (filename retains the v9_ 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.
  3. Provision both axonflow_app_role (NOBYPASSRLS) and axonflow_platform_admin (BYPASSRLS).
  4. Populate AXONFLOW_DB_APP_ROLE_URL and AXONFLOW_DB_PLATFORM_ADMIN_URL secrets, 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_id as the JSON field name. In v8 terminology this value is both your org_id and your client_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)

Read this before flipping AXONFLOW_DB_USE_APP_ROLE=true on a fork

If 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_idaudit_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:

PatternWhen to useAPI
Per-request org-scoped transactionMost application writes — the request carries an org_id in its auth contextWithOrgScope(ctx, db, orgID, func(tx *sql.Tx) error { … }) (agent) or withRequestOrgScope(r, h.db, fn) (customer-portal)
SECURITY DEFINER helperPre-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) poolCross-org workers (sweeps, mirrors, aggregators, tenant-delete) that genuinely iterate across orgsOpen 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.goRLSMiddleware, 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_heartbeats UPSERT under FORCE RLS — the agent's sendHeartbeat path wraps the UPSERT in an org-scoped transaction so the heartbeat write succeeds against the axonflow_app_role connection 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 / DELETE into 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 itPre-v8 namev8 conceptual nameOn-wire field name
/api/v1/register responsetenant_idclient_id (and org_id for Community-SaaS, where they share a value)unchanged: tenant_id
Stripe Plugin Pro checkout custom fieldtenant_idclient_idunchanged: tenant_id
HTTP request headerX-Tenant-IDX-Client-IDboth accepted; X-Client-ID is canonical
SDK config (TS/Py/Go/Java)clientIdclient_idunchanged: clientId (was already v8-correct)
Basic Auth usernamethe cs_* / your credential IDclient_idunchanged: same string
License payload fieldV2 used org_id onlyV3 adds deployment_id (canonical)V3 mints both deployment_id and org_id; agent prefers deployment_id. V2-only payloads still validate.
Audit row columnvariesorg_id (customer) + client_id (credential)new client_id column added; legacy tenant_id remains as deprecated alias

Header migration timeline

VersionX-Client-IDX-Tenant-IDAgent → orchestrator forwarding
Pre-v7.9not recognizedused internally for cross-process forwarding; tenant derived from Basic Auth on external requestsforwards X-Tenant-ID only
v7.9.xrecognized inbound + forwarded outboundaccepted as aliasforwards both X-Client-ID and X-Tenant-ID
v8.0.0 (current release)canonicalaccepted as deprecated aliasforwards both, with X-Client-ID as primary
v10 (planned, no date)requiredrejectedforwards 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_id and org_id on the V3 payload, with the same value. V2-only readers continue to validate signed payloads; V3-aware readers prefer deployment_id.
  • Exposes LicenseDeploymentID() accessor on the parsed license — call this in new code rather than reading .OrgID directly.
  • 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

Getting help

If a specific call started failing after a v8.0.0 upgrade, the most useful information for support:

  1. The exact endpoint (/api/v1/request, /api/v1/register, etc.).
  2. The Basic Auth clientId (first 8 chars only — never share the secret).
  3. Any X-*-ID headers you set (verbatim).
  4. The HTTP status code and response body.

[email protected] — please mention "v8 migration" in the subject so it routes correctly.