Skip to content

Commit 84ffd61

Browse files
committed
fix: add missing memberships_module:app dependencies to auth presets + add b2b:storage
Every auth preset (auth:email, auth:email+magic, auth:sso, auth:passkey, auth:hardened) included memberships_module:app but was missing the three modules it requires via NOT NULL FKs: - permissions_module:app (grants_table_id) - limits_module:app (caps_table_id) - levels_module:app (levels_table_id) Provisioning with any of these presets would fail with: null value in column "grants_table_id" of relation "memberships_module" violates not-null constraint Also adds b2b:storage preset (b2b + storage_module) for B2B apps that need file upload infrastructure. Proven empirically via CI failures in constructive-db PR constructive-io#1067.
1 parent 354ba0e commit 84ffd61

7 files changed

Lines changed: 109 additions & 13 deletions

File tree

packages/node-type-registry/src/module-presets/auth-email-magic.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const PresetAuthEmailMagic: ModulePreset = {
3636
modules: [
3737
'users_module',
3838
'membership_types_module',
39+
'permissions_module:app',
40+
'limits_module:app',
41+
'levels_module:app',
3942
'memberships_module:app',
4043
'sessions_module',
4144
'secrets_module',

packages/node-type-registry/src/module-presets/auth-email.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,29 @@ import type { ModulePreset } from './types';
1010
* `set_password`, `reset_password`, `forgot_password`, `verify_email`,
1111
* `delete_account`, `my_sessions`, API-key CRUD. Nothing more.
1212
*
13+
* Includes `permissions_module:app`, `limits_module:app`, and
14+
* `levels_module:app` because `memberships_module:app` has NOT NULL
15+
* foreign keys to the tables they create (grants, caps, levels).
16+
*
1317
* It deliberately excludes rate limits, connected accounts / identity
1418
* providers (OAuth), WebAuthn (passkeys), phone numbers (SMS), invites,
15-
* permissions, and org-scoped memberships. Bolt those on by moving to a
16-
* richer preset (`auth:hardened`, `b2b`) when you actually need them.
19+
* and org-scoped memberships. Bolt those on by moving to a richer preset
20+
* (`auth:hardened`, `b2b`) when you actually need them.
1721
*/
1822
export const PresetAuthEmail: ModulePreset = {
1923
name: 'auth:email',
2024
display_name: 'Email + Password',
21-
summary: 'Standard email/password auth flow. No orgs, no SSO, no MFA, no rate limits.',
25+
summary: 'Standard email/password auth flow with app-level permissions. No orgs, no SSO, no MFA.',
2226
description:
2327
'Installs `user_auth_module` with exactly the table dependencies its insert trigger ' +
24-
'hard-requires: users, app-scoped memberships, emails, secrets, encrypted secrets, ' +
25-
'sessions, plus RLS. You get the standard password-based auth procedures (sign_up, ' +
26-
"sign_in, reset_password, verify_email, delete_account, ...) and that's it. " +
27-
'Everything else in the module catalog — SSO, passkeys, SMS, rate limits, orgs, ' +
28-
'invites, permissions — is deliberately omitted. This is the right shape for single-tenant ' +
29-
'consumer apps in the first weeks, internal tools that need a real login, or anything ' +
30-
'where you want the lightest possible working auth and will add complexity only when ' +
31-
'forced to.',
28+
'hard-requires: users, app-scoped memberships (plus their permissions/limits/levels ' +
29+
'dependencies), emails, secrets, encrypted secrets, sessions, plus RLS. You get the ' +
30+
'standard password-based auth procedures (sign_up, sign_in, reset_password, ' +
31+
"verify_email, delete_account, ...) and that's it. Everything else in the module " +
32+
'catalog — SSO, passkeys, SMS, rate limits, orgs, invites — is deliberately omitted. ' +
33+
'This is the right shape for single-tenant consumer apps in the first weeks, internal ' +
34+
'tools that need a real login, or anything where you want the lightest possible working ' +
35+
'auth and will add complexity only when forced to.',
3236
good_for: [
3337
'Single-tenant consumer apps in the first week of development',
3438
'Internal tools where one simple login is enough',
@@ -43,6 +47,9 @@ export const PresetAuthEmail: ModulePreset = {
4347
modules: [
4448
'users_module',
4549
'membership_types_module',
50+
'permissions_module:app',
51+
'limits_module:app',
52+
'levels_module:app',
4653
'memberships_module:app',
4754
'sessions_module',
4855
'secrets_module',
@@ -54,6 +61,9 @@ export const PresetAuthEmail: ModulePreset = {
5461
includes_notes: {
5562
'memberships_module:app': 'Required by `user_auth_module`: every user gets an app-level membership row at sign-up.',
5663
membership_types_module: "Required by `memberships_module:app`; defines the 'app' scope.",
64+
'permissions_module:app': 'Required by `memberships_module:app`: NOT NULL FK to grants table.',
65+
'limits_module:app': 'Required by `memberships_module:app`: NOT NULL FK to caps table.',
66+
'levels_module:app': 'Required by `memberships_module:app`: NOT NULL FK to levels table.',
5767
emails_module: 'Required by the `user_auth_module` insert trigger (`RAISE EXCEPTION REQUIRES emails_module`).',
5868
encrypted_secrets_module: 'Required for password hashing; referenced by `set_password`, `verify_password`, and reset flows.',
5969
secrets_module: 'API-key storage (`create_api_key`, `revoke_api_key`, `my_api_keys`).'
@@ -65,7 +75,6 @@ export const PresetAuthEmail: ModulePreset = {
6575
webauthn_credentials_module: 'No passkeys — add `auth:passkey`.',
6676
phone_numbers_module: 'No SMS login — add `auth:hardened` or the SMS-only refactor path.',
6777
'memberships_module:org': 'No org/team structure — move to `b2b` when you need one.',
68-
'permissions_module:app': 'No fine-grained RBAC; the `is_admin` flag on users is the only gate.',
6978
invites_module: 'Self-serve signup only.',
7079
session_secrets_module: 'No magic-link / email-OTP nonces; add `auth:email+magic`.'
7180
}

packages/node-type-registry/src/module-presets/auth-hardened.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export const PresetAuthHardened: ModulePreset = {
3333
modules: [
3434
'users_module',
3535
'membership_types_module',
36+
'permissions_module:app',
37+
'limits_module:app',
38+
'levels_module:app',
3639
'memberships_module:app',
3740
'sessions_module',
3841
'secrets_module',
@@ -59,7 +62,6 @@ export const PresetAuthHardened: ModulePreset = {
5962
},
6063
omits_notes: {
6164
'memberships_module:org': 'No orgs / teams — use `b2b` when you need multi-tenancy.',
62-
'permissions_module:app': 'No RBAC beyond the `is_admin` flag — add via `b2b`.',
6365
invites_module: 'No invite flow — add via `b2b`.',
6466
storage_module: 'Add separately if you need file uploads.',
6567
crypto_addresses_module: 'Not a web3 preset; omit unless doing wallet sign-in.'

packages/node-type-registry/src/module-presets/auth-passkey.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export const PresetAuthPasskey: ModulePreset = {
3434
modules: [
3535
'users_module',
3636
'membership_types_module',
37+
'permissions_module:app',
38+
'limits_module:app',
39+
'levels_module:app',
3740
'memberships_module:app',
3841
'sessions_module',
3942
'secrets_module',

packages/node-type-registry/src/module-presets/auth-sso.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export const PresetAuthSso: ModulePreset = {
4343
modules: [
4444
'users_module',
4545
'membership_types_module',
46+
'permissions_module:app',
47+
'limits_module:app',
48+
'levels_module:app',
4649
'memberships_module:app',
4750
'sessions_module',
4851
'secrets_module',
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { ModulePreset } from './types';
2+
3+
/**
4+
* `b2b:storage` — everything in `b2b` plus `storage_module` for file uploads.
5+
*
6+
* This is the common shape for B2B SaaS apps that need file upload
7+
* infrastructure tied to their org/workspace structure. The storage module
8+
* creates `app_buckets` and `app_files` tables with RLS policies, and
9+
* entity-type-level storage scopes can be provisioned on top.
10+
*
11+
* If you don't need orgs, use a lighter preset and add `storage_module`
12+
* separately via provisioning options.
13+
*/
14+
export const PresetB2bStorage: ModulePreset = {
15+
name: 'b2b:storage',
16+
display_name: 'B2B SaaS + File Storage',
17+
summary: '`b2b` + file upload infrastructure (buckets, files, RLS).',
18+
description:
19+
'Everything in `b2b` (auth:hardened + orgs + invites + permissions + levels + profiles + ' +
20+
'hierarchy), plus `storage_module` for file uploads. The storage module creates ' +
21+
'`app_buckets` and `app_files` tables with full RLS: AuthzPublishable for public reads, ' +
22+
'AuthzAppMembership for member access, AuthzDirectOwner for uploader-only modify/delete. ' +
23+
'Entity-type provisioning with `has_storage=true` adds per-scope storage tables ' +
24+
'automatically. Choose this when your B2B app needs file uploads, avatars, attachments, ' +
25+
'or any object storage tied to workspaces.',
26+
good_for: [
27+
'B2B SaaS with file uploads (documents, avatars, attachments)',
28+
'Apps where storage is scoped to orgs/workspaces',
29+
'Apps that need per-entity-type file storage (e.g., project files, team assets)'
30+
],
31+
not_for: [
32+
'Single-tenant consumer apps — use `auth:email` or `auth:hardened` and add storage separately',
33+
'Apps without file upload needs — use `b2b` to avoid the storage table overhead'
34+
],
35+
modules: [
36+
'users_module',
37+
'membership_types_module',
38+
'permissions_module:app',
39+
'permissions_module:org',
40+
'limits_module:app',
41+
'limits_module:org',
42+
'levels_module:app',
43+
'levels_module:org',
44+
'memberships_module:app',
45+
'memberships_module:org',
46+
'sessions_module',
47+
'secrets_module',
48+
'encrypted_secrets_module',
49+
'emails_module',
50+
'rls_module',
51+
'user_auth_module',
52+
'session_secrets_module',
53+
'rate_limits_module',
54+
'connected_accounts_module',
55+
'identity_providers_module',
56+
'webauthn_credentials_module',
57+
'webauthn_auth_module',
58+
'phone_numbers_module',
59+
'profiles_module:app',
60+
'profiles_module:org',
61+
'hierarchy_module:org',
62+
'invites_module:app',
63+
'invites_module:org',
64+
'storage_module'
65+
],
66+
includes_notes: {
67+
storage_module: 'File upload infrastructure: app_buckets + app_files tables with RLS. Entity-type storage scopes layered on top via `has_storage=true`.'
68+
},
69+
omits_notes: {
70+
crypto_addresses_module: 'Not a web3 preset.'
71+
},
72+
extends: ['b2b']
73+
};

packages/node-type-registry/src/module-presets/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PresetAuthHardened } from './auth-hardened';
66
import { PresetAuthPasskey } from './auth-passkey';
77
import { PresetAuthSso } from './auth-sso';
88
import { PresetB2b } from './b2b';
9+
import { PresetB2bStorage } from './b2b-storage';
910
import { PresetFull } from './full';
1011
import { PresetMinimal } from './minimal';
1112
import type { ModulePreset } from './types';
@@ -17,6 +18,7 @@ export {
1718
PresetAuthPasskey,
1819
PresetAuthSso,
1920
PresetB2b,
21+
PresetB2bStorage,
2022
PresetFull,
2123
PresetMinimal};
2224

@@ -32,6 +34,7 @@ export const allModulePresets: ModulePreset[] = [
3234
PresetAuthPasskey,
3335
PresetAuthHardened,
3436
PresetB2b,
37+
PresetB2bStorage,
3538
PresetFull
3639
];
3740

0 commit comments

Comments
 (0)