Skip to content

Commit 3b12e8a

Browse files
committed
feat: separate superposition for provider
1 parent e45925d commit 3b12e8a

10 files changed

Lines changed: 38 additions & 79 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,10 @@ For current Keycloak-backed authorization, `AUTH_ADMIN_ISSUER` must be a Keycloa
309309

310310
| Variable | Required | Encrypted | Default | Description |
311311
|----------|----------|-----------|---------|-------------|
312-
| `SUPERPOSITION_URL` | Yes | No | - | Superposition service URL |
312+
| `SUPERPOSITION_URL` | Yes | No | - | Superposition service URL (control-plane calls) |
313+
| `SUPERPOSITION_RC_URL` | No | No | `SUPERPOSITION_URL` | Superposition service URL used to serve release config |
313314
| `SUPERPOSITION_ORG_ID` | Yes | No | - | Organization ID in Superposition |
314-
| `SUPERPOSITION_TOKEN` | No | **Yes** | - | Superposition API token |
315+
| `SUPERPOSITION_TOKEN` | No | **Yes** | - | Bearer token used to serve release config from `SUPERPOSITION_RC_URL` (for authenticated mode) |
315316
| `SUPERPOSITION_USER_TOKEN` | No | **Yes** | - | Superposition user token (for authenticated mode) |
316317
| `SUPERPOSITION_ORG_TOKEN` | No | **Yes** | - | Superposition organization token (for authenticated mode) |
317318
| `ENABLE_AUTHENTICATED_SUPERPOSITION` | No | No | `false` | Enable authenticated Superposition calls |

airborne_docs/docs/server/configuration.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,23 @@ Authorization uses Casbin, with policies persisted in Postgres. These variables
113113

114114
Airborne uses Superposition as its configuration/feature-flag engine for dimensions and release targeting. The base URL and org id are required; the token variables are only needed when Superposition is run in authenticated mode.
115115

116+
Airborne talks to Superposition through two independent paths, which may point at two different deployments. Control-plane writes (dimensions, releases, experiments) go through the Superposition SDK client at `SUPERPOSITION_URL` and authenticate with a cookie built from `SUPERPOSITION_USER_TOKEN` and `SUPERPOSITION_ORG_TOKEN`. Release-config serving (`/release`) resolves live against `SUPERPOSITION_RC_URL` through the provider registry, which authenticates with the `SUPERPOSITION_TOKEN` bearer token.
117+
116118
| Variable | Required | Default / Example | Purpose |
117119
| --- | --- | --- | --- |
118-
| `SUPERPOSITION_URL` | **Yes** | `http://localhost:8080` | Base URL of the Superposition service. |
120+
| `SUPERPOSITION_URL` | **Yes** | `http://localhost:8080` | Base URL of the Superposition service used for control-plane calls (dimensions, releases, experiments). |
121+
| `SUPERPOSITION_RC_URL` | No | _(falls back to `SUPERPOSITION_URL`)_ | Base URL of the Superposition service that release-config serving (`/release`) resolves against. Set this to point RC serving at a separate Superposition deployment. |
119122
| `SUPERPOSITION_ORG_ID` | **Yes** | `get-org-id-from-superposition` | Superposition organisation id. Populated by `init-superposition.sh` in dev. |
120-
| `ENABLE_AUTHENTICATED_SUPERPOSITION` | No | `false` | When `true`, the Superposition SDK client sends auth tokens/cookies; this makes the three token variables below mandatory. |
121-
| `SUPERPOSITION_TOKEN` | Conditional (secret) | _(unset)_ | Bearer token for Superposition. Used as the SDK bearer token (empty string if unset). |
122-
| `SUPERPOSITION_USER_TOKEN` | Conditional (secret) | _(unset)_ | User cookie token. **Required when `ENABLE_AUTHENTICATED_SUPERPOSITION=true`** (panics if missing in that mode). |
123-
| `SUPERPOSITION_ORG_TOKEN` | Conditional (secret) | _(unset)_ | Org cookie token. **Required when `ENABLE_AUTHENTICATED_SUPERPOSITION=true`** (panics if missing in that mode). |
123+
| `ENABLE_AUTHENTICATED_SUPERPOSITION` | No | `false` | When `true`, Superposition calls are authenticated; this makes the three token variables below mandatory. |
124+
| `SUPERPOSITION_TOKEN` | Conditional (secret) | _(unset)_ | Bearer token used by the release-config provider registry against `SUPERPOSITION_RC_URL`. **Required when `ENABLE_AUTHENTICATED_SUPERPOSITION=true`** (panics if missing in that mode). |
125+
| `SUPERPOSITION_USER_TOKEN` | Conditional (secret) | _(unset)_ | User cookie token for the SDK client against `SUPERPOSITION_URL`. **Required when `ENABLE_AUTHENTICATED_SUPERPOSITION=true`** (panics if missing in that mode). |
126+
| `SUPERPOSITION_ORG_TOKEN` | Conditional (secret) | _(unset)_ | Org cookie token for the SDK client against `SUPERPOSITION_URL`. **Required when `ENABLE_AUTHENTICATED_SUPERPOSITION=true`** (panics if missing in that mode). |
124127
| `SUPERPOSITION_CLEAR_UNUSED_PROVIDERS` | No | `false` | When `true`, a background task evicts idle per-workspace Superposition providers (see below). When `false`, providers live for the process lifetime. |
125128
| `SUPERPOSITION_UNUSED_PROVIDER_TTL` | No | `43200` | Idle time, in seconds, after which an unused workspace provider is evicted. Only applied when eviction is enabled. Default is 12 hours. |
126129
| `SUPERPOSITION_UNUSED_PROVIDER_CHECK_INTERVAL` | No | `1500` | How often, in seconds, the eviction sweep runs. Only applied when eviction is enabled. Default is 25 minutes. |
127130
| `SUPERPOSITION_MIGRATION_STRATEGY` | No | `PATCH` | Strategy used when reconciling Superposition default configs during the `superposition` boot migration. |
128131

129-
Airborne serves release config by resolving each workspace's (organisation + application) configuration live against the Superposition API. To avoid re-creating the SDK client on every request, the server caches one provider per workspace in an in-memory registry. Enabling `SUPERPOSITION_CLEAR_UNUSED_PROVIDERS` starts a background sweep that drops providers that have not been accessed within `SUPERPOSITION_UNUSED_PROVIDER_TTL`, checked every `SUPERPOSITION_UNUSED_PROVIDER_CHECK_INTERVAL`. This bounds memory on servers hosting many rarely-served workspaces.
132+
Airborne serves release config by resolving each workspace's (organisation + application) configuration live against the Superposition API at `SUPERPOSITION_RC_URL`. To avoid re-creating the SDK client on every request, the server caches one provider per workspace in an in-memory registry. Enabling `SUPERPOSITION_CLEAR_UNUSED_PROVIDERS` starts a background sweep that drops providers that have not been accessed within `SUPERPOSITION_UNUSED_PROVIDER_TTL`, checked every `SUPERPOSITION_UNUSED_PROVIDER_CHECK_INTERVAL`. This bounds memory on servers hosting many rarely-served workspaces.
130133

131134
:::warning[Use the plural env-var name]
132135
The server reads `SUPERPOSITION_CLEAR_UNUSED_PROVIDERS` (**plural**). The bundled `.env.example` currently writes it as `SUPERPOSITION_CLEAR_UNUSED_PROVIDER` (singular), which the server **ignores** — so a value set on the singular key has no effect. Always set the plural name.

airborne_server/.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ ENABLE_AUTHENTICATED_SUPERPOSITION=false
99
SUPERPOSITION_TOKEN=
1010
SUPERPOSITION_USER_TOKEN=
1111
SUPERPOSITION_ORG_TOKEN=
12-
SUPERPOSITION_RC_USER_TOKEN=
13-
SUPERPOSITION_RC_ORG_TOKEN=
1412

1513
# AuthN provider settings
1614
# AUTHN_PROVIDER can be "keycloak" (default), "oidc", "okta", or "auth0"

airborne_server/README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,12 +321,11 @@ The server relies on a set of environment variables for its configuration. These
321321
- `AUTH_ADMIN_SCOPES`: Optional space-separated scopes (commonly used for Okta/Auth0).
322322
- `AUTH_ADMIN_ISSUER`: Issuer URL used to derive Keycloak admin realm/base URL (`.../realms/<realm>`). Required for Keycloak signup/import flows.
323323
- `SUPERPOSITION_URL`: URL of the Superposition service.
324-
- `SUPERPOSITION_RC_URL`: URL of the Superposition service used by RC (`/release`) endpoints. Defaults to `SUPERPOSITION_URL` when unset.
325-
- `ENABLE_AUTHENTICATED_SUPERPOSITION`: Enables cookie-based auth for Superposition SDK requests.
326-
- `SUPERPOSITION_USER_TOKEN`: User token for Superposition auth cookie (`user=...`).
327-
- `SUPERPOSITION_ORG_TOKEN`: Org token for Superposition auth cookie (`org_<SUPERPOSITION_ORG_ID>=...`).
328-
- `SUPERPOSITION_RC_USER_TOKEN`: RC-specific user token for auth cookie; defaults to `SUPERPOSITION_USER_TOKEN` when unset.
329-
- `SUPERPOSITION_RC_ORG_TOKEN`: RC-specific org token for auth cookie; defaults to `SUPERPOSITION_ORG_TOKEN` when unset.
324+
- `SUPERPOSITION_RC_URL`: URL of the Superposition service that release-config (`/release`) serving resolves against. Defaults to `SUPERPOSITION_URL` when unset.
325+
- `ENABLE_AUTHENTICATED_SUPERPOSITION`: Enables authenticated Superposition calls (cookie auth for the SDK client, bearer auth for the release-config provider).
326+
- `SUPERPOSITION_USER_TOKEN`: User token for Superposition auth cookie (`user=...`), used by the SDK client against `SUPERPOSITION_URL`.
327+
- `SUPERPOSITION_ORG_TOKEN`: Org token for Superposition auth cookie (`org_<SUPERPOSITION_ORG_ID>=...`), used by the SDK client against `SUPERPOSITION_URL`.
328+
- `SUPERPOSITION_TOKEN`: Bearer token used by the release-config provider registry against `SUPERPOSITION_RC_URL`.
330329
- `SUPERPOSITION_ORG_ID`: The organization ID within Superposition used by the server.
331330
- `AWS_BUCKET`: Name of the S3 bucket for storing package assets.
332331
- `PUBLIC_ENDPOINT`: The public-facing URL for accessing assets stored in S3.

airborne_server/scripts/encrypt-envs.sh

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,12 @@ upsert_env_raw() {
125125

126126
sync_superposition_rc_env_defaults() {
127127
local superposition_url superposition_rc_url
128-
local superposition_user_token superposition_rc_user_token
129-
local superposition_org_token superposition_rc_org_token
130128

131129
superposition_url=$(read_env_raw "SUPERPOSITION_URL")
132130
superposition_rc_url=$(read_env_raw "SUPERPOSITION_RC_URL")
133131
if is_value_empty "$superposition_rc_url"; then
134132
upsert_env_raw "SUPERPOSITION_RC_URL" "$superposition_url"
135133
fi
136-
137-
superposition_user_token=$(read_env_raw "SUPERPOSITION_USER_TOKEN")
138-
superposition_rc_user_token=$(read_env_raw "SUPERPOSITION_RC_USER_TOKEN")
139-
if is_value_empty "$superposition_rc_user_token"; then
140-
upsert_env_raw "SUPERPOSITION_RC_USER_TOKEN" "$superposition_user_token"
141-
fi
142-
143-
superposition_org_token=$(read_env_raw "SUPERPOSITION_ORG_TOKEN")
144-
superposition_rc_org_token=$(read_env_raw "SUPERPOSITION_RC_ORG_TOKEN")
145-
if is_value_empty "$superposition_rc_org_token"; then
146-
upsert_env_raw "SUPERPOSITION_RC_ORG_TOKEN" "$superposition_org_token"
147-
fi
148134
}
149135

150136
# Function to encrypt a value using AES-GCM
@@ -187,8 +173,6 @@ SECRETS=(
187173
"SUPERPOSITION_TOKEN"
188174
"SUPERPOSITION_USER_TOKEN"
189175
"SUPERPOSITION_ORG_TOKEN"
190-
"SUPERPOSITION_RC_USER_TOKEN"
191-
"SUPERPOSITION_RC_ORG_TOKEN"
192176
"GOOGLE_SERVICE_ACCOUNT_KEY"
193177
)
194178

airborne_server/scripts/init-localstack.sh

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -159,26 +159,12 @@ is_value_empty() {
159159

160160
sync_superposition_rc_env_defaults() {
161161
local superposition_url superposition_rc_url
162-
local superposition_user_token superposition_rc_user_token
163-
local superposition_org_token superposition_rc_org_token
164162

165163
superposition_url=$(read_env_value "SUPERPOSITION_URL")
166164
superposition_rc_url=$(read_env_value "SUPERPOSITION_RC_URL")
167165
if is_value_empty "$superposition_rc_url"; then
168166
upsert_env_var ".env" "SUPERPOSITION_RC_URL" "$superposition_url"
169167
fi
170-
171-
superposition_user_token=$(read_env_value "SUPERPOSITION_USER_TOKEN")
172-
superposition_rc_user_token=$(read_env_value "SUPERPOSITION_RC_USER_TOKEN")
173-
if is_value_empty "$superposition_rc_user_token"; then
174-
upsert_env_var ".env" "SUPERPOSITION_RC_USER_TOKEN" "$superposition_user_token"
175-
fi
176-
177-
superposition_org_token=$(read_env_value "SUPERPOSITION_ORG_TOKEN")
178-
superposition_rc_org_token=$(read_env_value "SUPERPOSITION_RC_ORG_TOKEN")
179-
if is_value_empty "$superposition_rc_org_token"; then
180-
upsert_env_var ".env" "SUPERPOSITION_RC_ORG_TOKEN" "$superposition_org_token"
181-
fi
182168
}
183169

184170
sync_superposition_rc_env_defaults
@@ -220,8 +206,6 @@ SENSITIVE_VARS=(
220206
"SUPERPOSITION_TOKEN"
221207
"SUPERPOSITION_USER_TOKEN"
222208
"SUPERPOSITION_ORG_TOKEN"
223-
"SUPERPOSITION_RC_USER_TOKEN"
224-
"SUPERPOSITION_RC_ORG_TOKEN"
225209
)
226210

227211
# Get values from .env.example or .env.generated

airborne_server/src/config.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ pub struct AppConfig {
6767
pub superposition_token: Option<String>,
6868
pub superposition_user_token: Option<String>,
6969
pub superposition_org_token: Option<String>,
70-
pub superposition_rc_user_token: Option<String>,
71-
pub superposition_rc_org_token: Option<String>,
7270
pub enable_authenticated_superposition: bool,
7371
pub superposition_clear_unused_providers: bool,
7472
pub superposition_unused_provider_ttl: u64,
@@ -174,10 +172,6 @@ impl AppConfig {
174172
let superposition_token = get_optional_secret("SUPERPOSITION_TOKEN")?;
175173
let superposition_user_token = get_optional_secret("SUPERPOSITION_USER_TOKEN")?;
176174
let superposition_org_token = get_optional_secret("SUPERPOSITION_ORG_TOKEN")?;
177-
let superposition_rc_user_token = get_optional_secret("SUPERPOSITION_RC_USER_TOKEN")?
178-
.or_else(|| superposition_user_token.clone());
179-
let superposition_rc_org_token = get_optional_secret("SUPERPOSITION_RC_ORG_TOKEN")?
180-
.or_else(|| superposition_org_token.clone());
181175

182176
Ok(AppConfig {
183177
// Server settings
@@ -229,8 +223,6 @@ impl AppConfig {
229223
superposition_token,
230224
superposition_user_token,
231225
superposition_org_token,
232-
superposition_rc_user_token,
233-
superposition_rc_org_token,
234226
enable_authenticated_superposition: parse_env(
235227
"ENABLE_AUTHENTICATED_SUPERPOSITION",
236228
false,

airborne_server/src/main.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,6 @@ async fn main() -> std::io::Result<()> {
246246
.expect("Failed to complete Keycloak -> Casbin import");
247247
}
248248

249-
let superposition_token = app_config.superposition_token.clone().unwrap_or_default();
250-
251249
let dashboard_superposition_url = app_config.superposition_url.clone();
252250
let rc_superposition_url = app_config.superposition_rc_url.clone();
253251
let superposition_org_id_env = app_config.superposition_org_id.clone();
@@ -426,13 +424,6 @@ async fn main() -> std::io::Result<()> {
426424
"SUPERPOSITION_USER_TOKEN",
427425
"SUPERPOSITION_ORG_TOKEN",
428426
);
429-
let rc_superposition_client = create_superposition_client(
430-
rc_superposition_url,
431-
app_config.superposition_rc_user_token.clone(),
432-
app_config.superposition_rc_org_token.clone(),
433-
"SUPERPOSITION_RC_USER_TOKEN or SUPERPOSITION_USER_TOKEN",
434-
"SUPERPOSITION_RC_ORG_TOKEN or SUPERPOSITION_ORG_TOKEN",
435-
);
436427

437428
let authz_provider = build_authz_provider(
438429
authz_provider_kind,
@@ -460,16 +451,28 @@ async fn main() -> std::io::Result<()> {
460451
None
461452
};
462453

463-
let provider_url = cac_url + "/";
464-
let provider_access_token = format!(
465-
"user={}; org_{}={}",
466-
app_config.superposition_rc_user_token.clone(), superposition_org_id_env, app_config.superposition_rc_org_token.clone(),
467-
);
454+
let provider_url = if rc_superposition_url.ends_with('/') {
455+
rc_superposition_url.to_string()
456+
} else {
457+
format!("{rc_superposition_url}/")
458+
};
459+
// The provider registry authenticates with a bearer token (`AuthMethod::Token`);
460+
// it has no hook for the auth cookie the Superposition SDK client uses.
461+
let superposition_token = if app_config.enable_authenticated_superposition {
462+
app_config.superposition_token.clone().expect(
463+
"SUPERPOSITION_TOKEN must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true to use provider registry features",
464+
)
465+
} else {
466+
app_config
467+
.superposition_token
468+
.clone()
469+
.unwrap_or_else(|| "abcd".to_string())
470+
};
468471

469472
let provider_registry = Arc::new(ProviderRegistry::new(
470473
superposition_org_id_env.clone(),
471-
superposition_token.clone(),
472-
provider_url.clone(),
474+
superposition_token,
475+
provider_url,
473476
));
474477

475478
if app_config.superposition_clear_unused_providers {
@@ -495,7 +498,6 @@ async fn main() -> std::io::Result<()> {
495498
s3_client: aws_s3_client,
496499
cf_client: aws_cloudfront_client,
497500
superposition_client,
498-
rc_superposition_client,
499501
sheets_hub: hub,
500502
provider_registry,
501503
});

airborne_server/src/release.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,8 +1248,7 @@ async fn serve_release(
12481248
query: Query<ServeReleaseQueryParams>,
12491249
state: web::Data<AppState>,
12501250
) -> airborne_types::Result<WithHeaders<Json<ServeReleaseResponse>>> {
1251-
let superposition_client = state.rc_superposition_client.clone();
1252-
serve_release_handler(path, req, query, state, superposition_client).await
1251+
serve_release_handler(path, req, query, state).await
12531252
}
12541253

12551254
#[get("v2/{organisation}/{application}")]
@@ -1259,8 +1258,7 @@ async fn serve_release_v2(
12591258
query: Query<ServeReleaseQueryParams>,
12601259
state: web::Data<AppState>,
12611260
) -> airborne_types::Result<WithHeaders<Json<ServeReleaseResponse>>> {
1262-
let superposition_client = state.rc_superposition_client.clone();
1263-
serve_release_handler(path, req, query, state, superposition_client).await
1261+
serve_release_handler(path, req, query, state).await
12641262
}
12651263

12661264
async fn get_release_config_from_provider(
@@ -1290,7 +1288,6 @@ async fn serve_release_handler(
12901288
req: actix_web::HttpRequest,
12911289
query: Query<ServeReleaseQueryParams>,
12921290
state: web::Data<AppState>,
1293-
superposition_client: superposition_sdk::Client,
12941291
) -> airborne_types::Result<WithHeaders<Json<ServeReleaseResponse>>> {
12951292
let (organisation, application) = path.into_inner();
12961293

airborne_server/src/types.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ pub struct AppState {
4747
pub s3_client: aws_sdk_s3::Client,
4848
pub cf_client: aws_sdk_cloudfront::Client,
4949
pub superposition_client: Client,
50-
pub rc_superposition_client: Client,
5150
pub sheets_hub: Option<
5251
Sheets<hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>>,
5352
>,

0 commit comments

Comments
 (0)