diff --git a/packages/node-type-registry/src/blueprint-types.generated.ts b/packages/node-type-registry/src/blueprint-types.generated.ts index 519765c5a..d6501424e 100644 --- a/packages/node-type-registry/src/blueprint-types.generated.ts +++ b/packages/node-type-registry/src/blueprint-types.generated.ts @@ -1209,7 +1209,9 @@ export interface BlueprintBucketSeed { } /** Storage configuration for an entity type. Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions. */ export interface BlueprintStorageConfig { - /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. Only used for app-level storage (not entity-scoped). */ + /** Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case. */ + storage_key?: string; + /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. */ buckets?: BlueprintBucketSeed[]; /** Override for presigned upload URL expiry time in seconds. */ upload_url_expiry_seconds?: number; @@ -1300,8 +1302,7 @@ export interface BlueprintEntityType { has_profiles?: boolean; /** Whether to provision a levels module for this entity type. Defaults to false. */ has_levels?: boolean; - /** Whether to provision a storage module (buckets, files tables) for this entity type. Defaults to false. */ - has_storage?: boolean; + /** Whether to provision entity-scoped invite tables ({prefix}_invites, {prefix}_claimed_invites) and a submit_{prefix}_invite_code() function. Defaults to false. */ has_invites?: boolean; /** Whether to auto-attach an EventTracker to the claimed_invites table for invite-based achievements. Requires has_invites=true AND has_levels=true. When true, records 'invite_claimed' events credited to the sender (inviter) on each claimed invite. Defaults to false. */ @@ -1310,8 +1311,8 @@ export interface BlueprintEntityType { skip_entity_policies?: boolean; /** Override for the entity table. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, its policies[] replaces the five default entity-table policies; is_visible becomes a no-op. When NULL (default), the five default policies are applied (gated by is_visible). */ table_provision?: BlueprintEntityTableProvision; - /** Storage configuration. Only used when has_storage is true. Controls RLS policies on storage tables, seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). */ - storage?: BlueprintStorageConfig; + /** Storage configuration (array-only). A non-empty array enables storage provisioning. Each entry creates a separate storage module with its own tables ({prefix}_{storage_key}_buckets/files). Controls RLS policies, bucket seeding, and module-level settings. */ + storage?: BlueprintStorageConfig[]; } /** * =========================================================================== @@ -1594,8 +1595,8 @@ export interface BlueprintDefinition { unique_constraints?: BlueprintUniqueConstraint[]; /** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */ entity_types?: BlueprintEntityType[]; - /** App-level storage configuration. Creates a storage_module (membership_type = NULL), seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). Use provisions for per-table policy overrides. For entity-scoped storage, use entity_types[].has_storage + entity_types[].storage instead. */ - storage?: BlueprintStorageConfig; + /** App-level storage configuration (array-only). Creates storage_module(s) (membership_type = NULL), seeds initial buckets, and overrides module-level settings. Each entry creates a separate storage module. For entity-scoped storage, use entity_types[].storage instead. */ + storage?: BlueprintStorageConfig[]; /** Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module). */ achievements?: BlueprintAchievement[]; } diff --git a/packages/node-type-registry/src/codegen/generate-types.ts b/packages/node-type-registry/src/codegen/generate-types.ts index af6ef0dce..f902de99b 100644 --- a/packages/node-type-registry/src/codegen/generate-types.ts +++ b/packages/node-type-registry/src/codegen/generate-types.ts @@ -787,6 +787,10 @@ function buildBlueprintBucketSeed(): t.ExportNamedDeclaration { function buildBlueprintStorageConfig(): t.ExportNamedDeclaration { return addJSDoc( exportInterface('BlueprintStorageConfig', [ + addJSDoc( + optionalProp('storage_key', t.tsStringKeyword()), + 'Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case.' + ), addJSDoc( optionalProp( 'buckets', @@ -794,7 +798,7 @@ function buildBlueprintStorageConfig(): t.ExportNamedDeclaration { t.tsTypeReference(t.identifier('BlueprintBucketSeed')) ) ), - 'Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. Only used for app-level storage (not entity-scoped).' + 'Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning.' ), addJSDoc( optionalProp('upload_url_expiry_seconds', t.tsNumberKeyword()), @@ -1024,10 +1028,7 @@ function buildBlueprintEntityType(): t.ExportNamedDeclaration { optionalProp('has_levels', t.tsBooleanKeyword()), 'Whether to provision a levels module for this entity type. Defaults to false.' ), - addJSDoc( - optionalProp('has_storage', t.tsBooleanKeyword()), - 'Whether to provision a storage module (buckets, files tables) for this entity type. Defaults to false.' - ), + addJSDoc( optionalProp('has_invites', t.tsBooleanKeyword()), 'Whether to provision entity-scoped invite tables ({prefix}_invites, {prefix}_claimed_invites) and a submit_{prefix}_invite_code() function. Defaults to false.' @@ -1050,9 +1051,11 @@ function buildBlueprintEntityType(): t.ExportNamedDeclaration { addJSDoc( optionalProp( 'storage', - t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + t.tsArrayType( + t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + ) ), - 'Storage configuration. Only used when has_storage is true. Controls RLS policies on storage tables, seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS).' + 'Storage module configuration array. Each entry provisions a separate storage module with its own tables, RLS, and settings. When non-empty, has_storage is derived as true. Each entry may specify a storage_key for multi-module support (defaults to "default").' ) ]), 'An entity type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision.' @@ -1187,9 +1190,11 @@ function buildBlueprintDefinition(): t.ExportNamedDeclaration { addJSDoc( optionalProp( 'storage', - t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + t.tsArrayType( + t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + ) ), - 'App-level storage configuration. Creates a storage_module (membership_type = NULL), seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). Use provisions for per-table policy overrides. For entity-scoped storage, use entity_types[].has_storage + entity_types[].storage instead.' + 'App-level storage configuration array. Each entry creates a storage_module (membership_type = NULL) with its own tables and settings. For entity-scoped storage, use entity_types[].storage instead.' ), addJSDoc( optionalProp( diff --git a/packages/node-type-registry/src/module-presets/b2b-storage.ts b/packages/node-type-registry/src/module-presets/b2b-storage.ts index ac2b26925..8ae108321 100644 --- a/packages/node-type-registry/src/module-presets/b2b-storage.ts +++ b/packages/node-type-registry/src/module-presets/b2b-storage.ts @@ -20,9 +20,9 @@ export const PresetB2bStorage: ModulePreset = { 'hierarchy), plus `storage_module` for file uploads. The storage module creates ' + '`app_buckets` and `app_files` tables with full RLS: AuthzPublishable for public reads, ' + 'AuthzAppMembership for member access, AuthzDirectOwner for uploader-only modify/delete. ' + - 'Entity-type provisioning with `has_storage=true` adds per-scope storage tables ' + - 'automatically. Choose this when your B2B app needs file uploads, avatars, attachments, ' + - 'or any object storage tied to workspaces.', + 'Entity-type provisioning with a non-empty `storage` array adds per-scope storage tables ' + + 'automatically (multiple modules per entity via storage_key). Choose this when your B2B ' + + 'app needs file uploads, avatars, attachments, or any object storage tied to workspaces.', good_for: [ 'B2B SaaS with file uploads (documents, avatars, attachments)', 'Apps where storage is scoped to orgs/workspaces', @@ -65,7 +65,7 @@ export const PresetB2bStorage: ModulePreset = { 'devices_module' ], includes_notes: { - storage_module: 'File upload infrastructure: app_buckets + app_files tables with RLS. Entity-type storage scopes layered on top via `has_storage=true`.', + storage_module: 'File upload infrastructure: app_buckets + app_files tables with RLS. Entity-type storage scopes layered on top via the `storage` array (array-only format, supports multiple modules per entity via storage_key).', devices_module: 'Device tracking and trusted-device MFA bypass.' }, omits_notes: {