Skip to content

Commit 8e5d6b0

Browse files
committed
feat(webapp): pass database writer and reader config to the RBAC plugin
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 an installed RBAC plugin 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 per-request auth reads to the read replica instead of holding them on the primary.
1 parent 7faa525 commit 8e5d6b0

5 files changed

Lines changed: 48 additions & 2 deletions

File tree

apps/webapp/app/env.server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2072,6 +2072,12 @@ 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),

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
);

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

packages/plugins/src/index.ts

Lines changed: 1 addition & 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";

packages/plugins/src/rbac.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,23 @@ export type RbacPluginConfig = {
430430
// Platform secret the host signs user-actor tokens with; the plugin uses
431431
// it to verify them in `authenticateUserActor`. Omitted → UAT auth 401s.
432432
userActorSecret?: string;
433+
// Database connections for a plugin that owns its own client. The host
434+
// resolves the URLs from its env so the plugin follows the same
435+
// writer/replica topology the host uses. Omitted → the plugin falls back
436+
// to its own defaults.
437+
database?: RbacDatabaseConfig;
438+
};
439+
440+
export type RbacDatabaseConfig = {
441+
// Primary (writer) connection URL. Mutations and read-your-writes
442+
// management reads run here.
443+
writerUrl: string;
444+
// Read-replica URL for the per-request auth reads. Omitted → those reads
445+
// share the writer connection.
446+
readerUrl?: string;
447+
// Per-process pool sizes. Omitted → plugin defaults (writer 2, reader 5).
448+
writerConnectionLimit?: number;
449+
readerConnectionLimit?: number;
433450
};
434451

435452
export interface RoleBasedAccessControlPlugin {

0 commit comments

Comments
 (0)