Skip to content

Commit 057ff73

Browse files
committed
feat(webapp): pass database writer and reader config to auth plugins
The host now resolves writer and read-replica URLs from its env (the same fallback chain its own Prisma clients use) and hands them to installed RBAC and SSO plugins at create time, with separate connection limits for writes (default 2) and reads (default 5). A plugin that owns its own database client can then route hot-path reads (per-request auth checks, login routing) to the read replica instead of holding connections on the primary.
1 parent 48a0b83 commit 057ff73

9 files changed

Lines changed: 93 additions & 5 deletions

File tree

apps/webapp/app/env.server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2072,9 +2072,22 @@ const EnvironmentSchema = z
20722072
// Force RBAC to not use the plugin
20732073
RBAC_FORCE_FALLBACK: BoolEnv.default(false),
20742074

2075+
// Per-process pool sizes for an RBAC plugin that owns its own database
2076+
// client (the fallback queries through Prisma and ignores these). Writes
2077+
// are rare role mutations; reads run on the per-request auth hot path.
2078+
RBAC_DATABASE_WRITER_CONNECTION_LIMIT: z.coerce.number().int().default(2),
2079+
RBAC_DATABASE_READER_CONNECTION_LIMIT: z.coerce.number().int().default(5),
2080+
20752081
// Force SSO to not use the plugin (contributors without the cloud
20762082
// plugin installed can opt in to a clean OSS-only experience).
20772083
SSO_FORCE_FALLBACK: BoolEnv.default(false),
2084+
2085+
// Per-process pool sizes for an SSO plugin that owns its own database
2086+
// client (the fallback queries through Prisma and ignores these). Writes
2087+
// are rare config mutations and webhook processing; reads run on the
2088+
// login path.
2089+
SSO_DATABASE_WRITER_CONNECTION_LIMIT: z.coerce.number().int().default(2),
2090+
SSO_DATABASE_READER_CONNECTION_LIMIT: z.coerce.number().int().default(5),
20782091
// Emit a console.log when the SSO fallback is selected because no
20792092
// plugin is installed. Default off so OSS deployments stay quiet.
20802093
SSO_LOG_FALLBACK: BoolEnv.default(false),

apps/webapp/app/services/rbac.server.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,18 @@ export const rbac = plugin.create(
2727
{ primary: prisma, replica: $replica as PrismaClient },
2828
// SESSION_SECRET signs delegated user-actor tokens; the plugin verifies
2929
// them with it in authenticateUserActor.
30-
{ forceFallback: env.RBAC_FORCE_FALLBACK, userActorSecret: env.SESSION_SECRET }
30+
{
31+
forceFallback: env.RBAC_FORCE_FALLBACK,
32+
userActorSecret: env.SESSION_SECRET,
33+
// A plugin that owns its own database client gets the same
34+
// writer/replica topology the webapp's Prisma clients use (see
35+
// getClient/getReplicaClient in db.server.ts): control-plane URLs win,
36+
// and with no replica configured reads share the writer.
37+
database: {
38+
writerUrl: env.CONTROL_PLANE_DATABASE_URL ?? env.DATABASE_URL,
39+
readerUrl: env.CONTROL_PLANE_DATABASE_READ_REPLICA_URL ?? env.DATABASE_READ_REPLICA_URL,
40+
writerConnectionLimit: env.RBAC_DATABASE_WRITER_CONNECTION_LIMIT,
41+
readerConnectionLimit: env.RBAC_DATABASE_READER_CONNECTION_LIMIT,
42+
},
43+
}
3144
);

apps/webapp/app/services/sso.server.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,17 @@ export const ssoController = sso.create(
1818
// fallback so the entire SSO surface (login, settings, callback,
1919
// re-validation) stays inert. SSO_FORCE_FALLBACK remains an
2020
// independent contributor/debug override.
21-
{ forceFallback: !env.SSO_ENABLED || env.SSO_FORCE_FALLBACK }
21+
{
22+
forceFallback: !env.SSO_ENABLED || env.SSO_FORCE_FALLBACK,
23+
// A plugin that owns its own database client gets the same
24+
// writer/replica topology the webapp's Prisma clients use (see
25+
// getClient/getReplicaClient in db.server.ts): control-plane URLs win,
26+
// and with no replica configured reads share the writer.
27+
database: {
28+
writerUrl: env.CONTROL_PLANE_DATABASE_URL ?? env.DATABASE_URL,
29+
readerUrl: env.CONTROL_PLANE_DATABASE_READ_REPLICA_URL ?? env.DATABASE_READ_REPLICA_URL,
30+
writerConnectionLimit: env.SSO_DATABASE_WRITER_CONNECTION_LIMIT,
31+
readerConnectionLimit: env.SSO_DATABASE_READER_CONNECTION_LIMIT,
32+
},
33+
}
2234
);

internal-packages/rbac/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
Permission,
33
RbacAbility,
4+
RbacDatabaseConfig,
45
Role,
56
RbacResource,
67
RoleAssignmentResult,
@@ -33,6 +34,11 @@ export type RbacCreateOptions = {
3334
// Platform secret used to verify delegated user-actor tokens (tr_uat_).
3435
// Threaded through to the plugin / fallback's authenticateUserActor.
3536
userActorSecret?: string;
37+
// Writer/reader connection URLs + pool sizes for a plugin that owns its
38+
// own database client, resolved by the host from its env so the plugin
39+
// follows the host's writer/replica topology. The fallback ignores this —
40+
// it queries through the Prisma clients passed as `RbacPrismaInput`.
41+
database?: RbacDatabaseConfig;
3642
};
3743

3844
// Route actions that historically authorised via the legacy checkAuthorization's
@@ -88,7 +94,10 @@ class LazyController implements RoleBaseAccessController {
8894
const module = await import(moduleName);
8995
const plugin: RoleBasedAccessControlPlugin = module.default;
9096
console.log("RBAC: using plugin implementation");
91-
return plugin.create({ userActorSecret: options?.userActorSecret });
97+
return plugin.create({
98+
userActorSecret: options?.userActorSecret,
99+
database: options?.database,
100+
});
92101
} catch (err) {
93102
// The dynamic import either succeeded or failed for one of two
94103
// distinct reasons. Distinguishing them is critical for debugging

internal-packages/sso/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
DirectorySyncEffect,
33
DirectorySyncStatus,
44
OrgSsoStatus,
5+
PluginDatabaseConfig,
56
SsoBeginError,
67
SsoCompleteError,
78
SsoController,
@@ -32,6 +33,11 @@ export type SsoCreateOptions = {
3233
// module or a synthetic ERR_MODULE_NOT_FOUND failure without touching
3334
// the real plugin install on disk.
3435
importer?: (moduleName: string) => Promise<{ default: SsoPlugin }>;
36+
// Writer/reader connection URLs + pool sizes for a plugin that owns its
37+
// own database client, resolved by the host from its env so the plugin
38+
// follows the host's writer/replica topology. The fallback ignores this —
39+
// it queries through the Prisma clients passed as `SsoPrismaInput`.
40+
database?: PluginDatabaseConfig;
3541
};
3642

3743
// Loads the cloud plugin lazily; falls back to the OSS no-op
@@ -55,7 +61,7 @@ export class LazyController implements SsoController {
5561
const module = await importer(moduleName);
5662
const plugin: SsoPlugin = module.default;
5763
console.log("SSO: using plugin implementation");
58-
return plugin.create();
64+
return plugin.create({ database: options?.database });
5965
} catch (err) {
6066
// Distinguish the two failure modes the dynamic import can hit:
6167
//
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Database connections for a plugin that owns its own client. The host
2+
// resolves the URLs from its env so the plugin follows the same
3+
// writer/replica topology the host uses. Shared by every plugin contract
4+
// that carries a `database` section.
5+
export type PluginDatabaseConfig = {
6+
// Primary (writer) connection URL. Mutations and read-your-writes
7+
// management reads run here.
8+
writerUrl: string;
9+
// Read-replica URL for the per-request auth reads. Omitted → those reads
10+
// share the writer connection.
11+
readerUrl?: string;
12+
// Per-process pool sizes. Omitted → plugin defaults (writer 2, reader 5).
13+
writerConnectionLimit?: number;
14+
readerConnectionLimit?: number;
15+
};

packages/plugins/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type {
1616
UserActorAuthResult,
1717
UserActorClaims,
1818
RbacPluginConfig,
19+
RbacDatabaseConfig,
1920
SystemRole,
2021
AuthenticatedEnvironment,
2122
} from "./rbac.js";
@@ -28,8 +29,11 @@ export {
2829
USER_ACTOR_TOKEN_PREFIX,
2930
} from "./rbac.js";
3031

32+
export type { PluginDatabaseConfig } from "./databaseConfig.js";
33+
3134
export type {
3235
SsoPlugin,
36+
SsoPluginConfig,
3337
SsoController,
3438
OrgSsoStatus,
3539
SsoRouteDecision,

packages/plugins/src/rbac.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,14 +424,21 @@ export type RoleMutationResult = { ok: true; role: Role } | { ok: false; error:
424424
// Result for assignment / deletion mutations that don't return a value.
425425
export type RoleAssignmentResult = { ok: true } | { ok: false; error: string };
426426

427+
import type { PluginDatabaseConfig } from "./databaseConfig.js";
428+
427429
// Host-injected configuration the plugin can't read from the environment
428430
// itself (the plugin runs in the host's process but owns no env contract).
429431
export type RbacPluginConfig = {
430432
// Platform secret the host signs user-actor tokens with; the plugin uses
431433
// it to verify them in `authenticateUserActor`. Omitted → UAT auth 401s.
432434
userActorSecret?: string;
435+
// Database connections for a plugin that owns its own client. Omitted →
436+
// the plugin falls back to its own defaults.
437+
database?: PluginDatabaseConfig;
433438
};
434439

440+
export type { PluginDatabaseConfig as RbacDatabaseConfig } from "./databaseConfig.js";
441+
435442
export interface RoleBasedAccessControlPlugin {
436443
create(config?: RbacPluginConfig): RoleBaseAccessController | Promise<RoleBaseAccessController>;
437444
}

packages/plugins/src/sso.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ResultAsync } from "neverthrow";
2+
import type { PluginDatabaseConfig } from "./databaseConfig.js";
23

34
// === Domain types ===
45

@@ -368,6 +369,14 @@ export interface SsoController {
368369
): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoWebhookError>;
369370
}
370371

372+
// Host-injected configuration the plugin can't read from the environment
373+
// itself (the plugin runs in the host's process but owns no env contract).
374+
export type SsoPluginConfig = {
375+
// Database connections for a plugin that owns its own client. Omitted →
376+
// the plugin falls back to its own defaults.
377+
database?: PluginDatabaseConfig;
378+
};
379+
371380
export interface SsoPlugin {
372-
create(): SsoController | Promise<SsoController>;
381+
create(config?: SsoPluginConfig): SsoController | Promise<SsoController>;
373382
}

0 commit comments

Comments
 (0)