From f2ef24ee0710eb7c485a2cd9df829738eb3c95e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 09:51:02 +0000 Subject: [PATCH 1/3] spec: re-type TenantPlan / sys_environment.plan as an opaque plan identifier Widens `TenantPlanSchema` from a closed 5-value enum (free/starter/pro/ enterprise/custom) to an opaque string. The vocabulary is control-plane config owned by the cloud distribution, not protocol -- the schema's .describe() now states that ownership and the shared empty/unknown => free tier convention. Executes the OPAQUE arm of the maintainer ruling on cloud#1216 (2026-08-10), whose one open condition -- "does any reader branch on plan values?" -- was measured (cloud seat, 2026-08-11) and resolved: no reader outside the cloud distribution branches on plan values. Re-verified against origin/main before implementing (empty framework-consumer grep, positive control hits spec files). Pure widening: every value the old enum accepted is still accepted, no runtime parser depended on the closed set. The pre-existing pin test asserting the closed enum is flipped, not deleted, to assert the new contract's substance (opaque acceptance including a cloud-vocabulary value the old enum rejected, continued rejection of non-string shapes, and the describe() carrying the ownership + convention statements). Regenerates content/docs/references/cloud/{tenant,environment}.mdx via `pnpm --filter @objectstack/spec gen:docs` (plan field type: Enum<...> -> string). Fixes #7513 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk --- .changeset/tenant-plan-opaque-widening.md | 18 ++++++ content/docs/references/cloud/environment.mdx | 6 +- content/docs/references/cloud/tenant.mdx | 16 +----- packages/spec/src/cloud/tenant.test.ts | 55 +++++++++++++++++-- packages/spec/src/cloud/tenant.zod.ts | 32 ++++++++--- 5 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 .changeset/tenant-plan-opaque-widening.md diff --git a/.changeset/tenant-plan-opaque-widening.md b/.changeset/tenant-plan-opaque-widening.md new file mode 100644 index 0000000000..d3d3af611a --- /dev/null +++ b/.changeset/tenant-plan-opaque-widening.md @@ -0,0 +1,18 @@ +--- +"@objectstack/spec": minor +--- + +Widen `TenantPlanSchema` / `TenantPlan` (`packages/spec/src/cloud/tenant.zod.ts`) from a +closed 5-value enum (`free`/`starter`/`pro`/`enterprise`/`custom`) to an opaque plan +identifier (any string). The vocabulary — which plan strings exist and what each one +unlocks — is control-plane config owned by the cloud distribution, not protocol; the +schema's `.describe()` states this ownership and the shared empty/unknown-⇒-free-tier +convention. + +This is a pure widening: every value the old enum accepted is still accepted, and nothing +in the framework, console, or spec package parses this field against the enum at runtime +(measured in cloud#1216 / objectstack#7513 — the only branching readers are the cloud +distribution's own entitlement modules). No migration is required; `sys_environment.plan` +and the other embedding schemas (`TenantContext`, `TenantDatabase`, +`ProvisionTenantRequest`, `ProvisionEnvironmentRequest`, `ProvisionOrganizationRequest`) +keep their field-level shape and defaults unchanged. diff --git a/content/docs/references/cloud/environment.mdx b/content/docs/references/cloud/environment.mdx index 663d2c76d8..22e3a728c0 100644 --- a/content/docs/references/cloud/environment.mdx +++ b/content/docs/references/cloud/environment.mdx @@ -57,7 +57,7 @@ const result = EnvironmentSchema.parse(data); | **displayName** | `string` | ✅ | Display name shown in Studio and APIs | | **isDefault** | `boolean` | ✅ | Whether this is the default environment for the organization | | **isSystem** | `boolean` | ✅ | Whether this is a system environment (platform infrastructure, not user data) | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Plan tier for this environment | +| **plan** | `string` | ✅ | Plan tier for this environment | | **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed' \| 'migrating'>` | ✅ | Environment lifecycle status | | **createdBy** | `string` | ✅ | User ID that created the environment | | **createdAt** | `string` | ✅ | Creation timestamp (ISO-8601) | @@ -197,7 +197,7 @@ Public exposure of this environment artifacts (private | unlisted | public). | **organizationId** | `string` | ✅ | Organization that will own the new environment | | **displayName** | `string` | ✅ | Display name shown in Studio and APIs | | **driver** | `string` | optional | Driver key (defaults to provisioning service config) | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | optional | Plan tier | +| **plan** | `string` | optional | Plan tier | | **storageLimitMb** | `integer` | optional | Storage quota in megabytes | | **isDefault** | `boolean` | optional | Mark as the organization default environment | | **createdBy** | `string` | ✅ | User ID that initiated the provisioning | @@ -233,7 +233,7 @@ Public exposure of this environment artifacts (private | unlisted | public). | **organizationId** | `string` | ✅ | Organization being bootstrapped | | **defaultEnvironmentDisplayName** | `string` | ✅ | Display name for the default environment | | **driver** | `string` | optional | Driver key | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | optional | Plan tier | +| **plan** | `string` | optional | Plan tier | | **storageLimitMb** | `integer` | optional | Storage quota in megabytes | | **createdBy** | `string` | ✅ | User ID that initiated provisioning | | **metadata** | `Record` | optional | Free-form metadata | diff --git a/content/docs/references/cloud/tenant.mdx b/content/docs/references/cloud/tenant.mdx index c3d4769962..e11dbd70e5 100644 --- a/content/docs/references/cloud/tenant.mdx +++ b/content/docs/references/cloud/tenant.mdx @@ -72,7 +72,7 @@ const result = PackageInstallationSchema.parse(data); | :--- | :--- | :--- | :--- | | **organizationId** | `string` | ✅ | Organization ID | | **region** | `string` | optional | Deployment region preference | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | +| **plan** | `string` | ✅ | Tenant plan tier | | **storageLimitMb** | `integer` | optional | Storage limit in megabytes | | **metadata** | `Record` | optional | Custom tenant metadata | @@ -102,7 +102,7 @@ const result = PackageInstallationSchema.parse(data); | **organizationId** | `string` | ✅ | Current organization ID | | **organizationSlug** | `string` | optional | Organization slug | | **databaseUrl** | `string` | ✅ | Tenant database URL | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | +| **plan** | `string` | ✅ | Tenant plan tier | | **metadata** | `Record` | optional | Custom tenant metadata | @@ -121,7 +121,7 @@ const result = PackageInstallationSchema.parse(data); | **authToken** | `string` | ✅ | Encrypted tenant-specific auth token | | **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed'>` | ✅ | Database status | | **region** | `string` | ✅ | Deployment region | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | +| **plan** | `string` | ✅ | Tenant plan tier | | **storageLimitMb** | `integer` | ✅ | Storage limit in megabytes | | **createdAt** | `string` | ✅ | Database creation timestamp | | **updatedAt** | `string` | ✅ | Last update timestamp | @@ -158,16 +158,6 @@ const result = PackageInstallationSchema.parse(data); --- -## TenantPlan - -### Allowed Values - -* `free` -* `starter` -* `pro` -* `enterprise` -* `custom` - --- diff --git a/packages/spec/src/cloud/tenant.test.ts b/packages/spec/src/cloud/tenant.test.ts index 6a43f8cac4..8742134be8 100644 --- a/packages/spec/src/cloud/tenant.test.ts +++ b/packages/spec/src/cloud/tenant.test.ts @@ -119,9 +119,9 @@ describe('[#4739] `TenantPlan(Schema)` resolves to the ./cloud declaration every expect('ProvisioningStepSchema' in system).toBe(false); }); - it('what the name now unambiguously means: the 5-value cloud plan vocabulary', () => { - // The full surviving vocabulary — an enum def has no authorable keys, so - // the vocabulary IS the surface this pin protects. + it('what the name now unambiguously means: the cloud declaration, opaque (objectstack#7513)', () => { + // The former 5-value enum is a strict subset of "any string" — still + // accepted, but no longer the boundary of what's valid. for (const plan of ['free', 'starter', 'pro', 'enterprise', 'custom']) { expect(() => TenantPlanSchema.parse(plan)).not.toThrow(); } @@ -129,8 +129,51 @@ describe('[#4739] `TenantPlan(Schema)` resolves to the ./cloud declaration every // rejected: their acceptance proves the survivor is the cloud declaration, // not the provisioning one. expect(TenantPlanSchema.parse('starter')).toBe('starter'); - // And it is still a closed enum, not an open string. - expect(() => TenantPlanSchema.parse('solo')).toThrow(); - expect(() => TenantPlanSchema.parse('')).toThrow(); + }); + + // ─── [objectstack#7513] opaque widening — cloud#1216's measured "no reader + // outside the cloud distribution branches on plan values" ──────────────── + // + // Flips (does not delete) the pre-#7513 pin that asserted the enum's closed + // set. That pin protected the CLOSED-SET boundary; this one protects the + // NEW contract's substance: any string is accepted (the cloud distribution + // owns the vocabulary, not this schema), and the schema's own `.describe()` + // still states that ownership so a reader who only sees the generated + // reference docs (not this source file) gets the same fact. + describe('[objectstack#7513] TenantPlanSchema is an opaque string, not a closed enum', () => { + it('accepts values the retired enum would have rejected — the widening is real', () => { + // `solo` is one of the CLOUD distribution's own vocabulary values + // (`packages/objectos-runtime/src/plan-entitlements.ts` in the cloud + // repo) — exactly the kind of value this widening exists to accept + // without a spec-side edit. + expect(() => TenantPlanSchema.parse('solo')).not.toThrow(); + expect(TenantPlanSchema.parse('solo')).toBe('solo'); + // Arbitrary/unknown strings — the vocabulary is cloud config, not a + // spec-enumerated set, so nothing here is "invalid" on shape grounds. + expect(() => TenantPlanSchema.parse('anything-the-cloud-distribution-declares')).not.toThrow(); + // The empty string is accepted too — free-tier fallback is a cloud-side + // normalization convention, not a spec-level rejection or default. + expect(() => TenantPlanSchema.parse('')).not.toThrow(); + }); + + it('still rejects non-string shapes — opaque means "any string", not "any value"', () => { + expect(() => TenantPlanSchema.parse(42)).toThrow(); + expect(() => TenantPlanSchema.parse(null)).toThrow(); + expect(() => TenantPlanSchema.parse(undefined)).toThrow(); + expect(() => TenantPlanSchema.parse({ plan: 'free' })).toThrow(); + }); + + it('the describe() states the ownership + convention, for readers who only see generated docs', () => { + const description = TenantPlanSchema.description; + expect(description, 'TenantPlanSchema must carry a .describe()').toBeDefined(); + // Ownership statement (ask #1): vocabulary is control-plane config, + // owned by the cloud distribution — not protocol. + expect(description).toMatch(/control-plane config/); + expect(description).toMatch(/cloud distribution/); + expect(description).toMatch(/not protocol/); + // Empty/unknown ⇒ free tier convention (ask #2) — one sentence worth + // keeping, not enforced by this schema. + expect(description).toMatch(/free tier/); + }); }); }); diff --git a/packages/spec/src/cloud/tenant.zod.ts b/packages/spec/src/cloud/tenant.zod.ts index adff8d6e34..5c6e0e7758 100644 --- a/packages/spec/src/cloud/tenant.zod.ts +++ b/packages/spec/src/cloud/tenant.zod.ts @@ -30,15 +30,31 @@ export const TenantDatabaseStatusSchema = lazySchema(() => z.enum([ export type TenantDatabaseStatus = z.input; /** - * Tenant Plan Tier + * Tenant Plan Identifier + * + * An **opaque** plan/tier identifier (ruling: cloud#1216, executed on the + * spec side by objectstack#7513). The vocabulary — which strings exist, what + * each one unlocks — is control-plane **config owned by the cloud + * distribution**, not protocol: this schema does not enumerate it and never + * has runtime knowledge of it. Widened from a closed 5-value enum + * (`free`/`starter`/`pro`/`enterprise`/`custom`) after measuring that no + * reader outside the cloud distribution branches on the value — framework + * and console treat it as a pass-through string; only the cloud + * distribution's own entitlement modules (e.g. `isFreePlan`, + * `planAllowsAiStudio`) interpret specific values, and they own that + * interpretation independently of this schema. + * + * Convention (not enforced here): an empty or unrecognized value is treated + * as the free tier by cloud-side readers. Spec accepts any string, including + * the empty one — the free-tier fallback is the cloud distribution's + * normalization, not a spec-level default. */ -export const TenantPlanSchema = lazySchema(() => z.enum([ - 'free', - 'starter', - 'pro', - 'enterprise', - 'custom', -])); +export const TenantPlanSchema = lazySchema(() => z.string().describe( + 'Opaque plan/tier identifier. The vocabulary is control-plane config owned by the ' + + 'cloud distribution, not protocol — this schema accepts any string and does not ' + + 'enumerate valid values. Convention: an empty or unrecognized value is treated as ' + + 'the free tier by cloud-side readers (not enforced by this schema).', +)); export type TenantPlan = z.input; From 5f23206f93849f2e90c0ca4f6e6c5ccadbef4aaf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 10:08:21 +0000 Subject: [PATCH 2/3] chore: reset os-regen-routed paths to origin/main post-merge Pre-sync for the merge queue (PM instruction on #7513): the os-regen merge driver exits 0 with zero conflict markers while silently able to drop one side, so force every merge=os-regen path (.gitattributes) to origin/main's exact content rather than trust the driver's merge output. Only content/docs/references/cloud/{tenant,environment}.mdx differed (this branch's own regenerated docs) -- everything else already matched origin/main byte-for-byte. The full gen pipeline regenerates the two touched docs pages from source next. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk --- content/docs/references/cloud/environment.mdx | 6 +++--- content/docs/references/cloud/tenant.mdx | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/content/docs/references/cloud/environment.mdx b/content/docs/references/cloud/environment.mdx index 22e3a728c0..663d2c76d8 100644 --- a/content/docs/references/cloud/environment.mdx +++ b/content/docs/references/cloud/environment.mdx @@ -57,7 +57,7 @@ const result = EnvironmentSchema.parse(data); | **displayName** | `string` | ✅ | Display name shown in Studio and APIs | | **isDefault** | `boolean` | ✅ | Whether this is the default environment for the organization | | **isSystem** | `boolean` | ✅ | Whether this is a system environment (platform infrastructure, not user data) | -| **plan** | `string` | ✅ | Plan tier for this environment | +| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Plan tier for this environment | | **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed' \| 'migrating'>` | ✅ | Environment lifecycle status | | **createdBy** | `string` | ✅ | User ID that created the environment | | **createdAt** | `string` | ✅ | Creation timestamp (ISO-8601) | @@ -197,7 +197,7 @@ Public exposure of this environment artifacts (private | unlisted | public). | **organizationId** | `string` | ✅ | Organization that will own the new environment | | **displayName** | `string` | ✅ | Display name shown in Studio and APIs | | **driver** | `string` | optional | Driver key (defaults to provisioning service config) | -| **plan** | `string` | optional | Plan tier | +| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | optional | Plan tier | | **storageLimitMb** | `integer` | optional | Storage quota in megabytes | | **isDefault** | `boolean` | optional | Mark as the organization default environment | | **createdBy** | `string` | ✅ | User ID that initiated the provisioning | @@ -233,7 +233,7 @@ Public exposure of this environment artifacts (private | unlisted | public). | **organizationId** | `string` | ✅ | Organization being bootstrapped | | **defaultEnvironmentDisplayName** | `string` | ✅ | Display name for the default environment | | **driver** | `string` | optional | Driver key | -| **plan** | `string` | optional | Plan tier | +| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | optional | Plan tier | | **storageLimitMb** | `integer` | optional | Storage quota in megabytes | | **createdBy** | `string` | ✅ | User ID that initiated provisioning | | **metadata** | `Record` | optional | Free-form metadata | diff --git a/content/docs/references/cloud/tenant.mdx b/content/docs/references/cloud/tenant.mdx index e11dbd70e5..c3d4769962 100644 --- a/content/docs/references/cloud/tenant.mdx +++ b/content/docs/references/cloud/tenant.mdx @@ -72,7 +72,7 @@ const result = PackageInstallationSchema.parse(data); | :--- | :--- | :--- | :--- | | **organizationId** | `string` | ✅ | Organization ID | | **region** | `string` | optional | Deployment region preference | -| **plan** | `string` | ✅ | Tenant plan tier | +| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | | **storageLimitMb** | `integer` | optional | Storage limit in megabytes | | **metadata** | `Record` | optional | Custom tenant metadata | @@ -102,7 +102,7 @@ const result = PackageInstallationSchema.parse(data); | **organizationId** | `string` | ✅ | Current organization ID | | **organizationSlug** | `string` | optional | Organization slug | | **databaseUrl** | `string` | ✅ | Tenant database URL | -| **plan** | `string` | ✅ | Tenant plan tier | +| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | | **metadata** | `Record` | optional | Custom tenant metadata | @@ -121,7 +121,7 @@ const result = PackageInstallationSchema.parse(data); | **authToken** | `string` | ✅ | Encrypted tenant-specific auth token | | **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed'>` | ✅ | Database status | | **region** | `string` | ✅ | Deployment region | -| **plan** | `string` | ✅ | Tenant plan tier | +| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | | **storageLimitMb** | `integer` | ✅ | Storage limit in megabytes | | **createdAt** | `string` | ✅ | Database creation timestamp | | **updatedAt** | `string` | ✅ | Last update timestamp | @@ -158,6 +158,16 @@ const result = PackageInstallationSchema.parse(data); --- +## TenantPlan + +### Allowed Values + +* `free` +* `starter` +* `pro` +* `enterprise` +* `custom` + --- From 47aa83657ea9be29c5337d67f4155bad36d18cbb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 10:14:58 +0000 Subject: [PATCH 3/3] chore: regenerate docs after os-regen pre-sync Full gen pipeline run post-merge (pnpm --filter @objectstack/spec build, then gen:docs) reproduces this branch's tenant/environment reference-doc changes from source, confirming check:generated is clean against the merged tree (all 13 artifacts up to date). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk --- content/docs/references/cloud/environment.mdx | 6 +++--- content/docs/references/cloud/tenant.mdx | 16 +++------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/content/docs/references/cloud/environment.mdx b/content/docs/references/cloud/environment.mdx index 663d2c76d8..22e3a728c0 100644 --- a/content/docs/references/cloud/environment.mdx +++ b/content/docs/references/cloud/environment.mdx @@ -57,7 +57,7 @@ const result = EnvironmentSchema.parse(data); | **displayName** | `string` | ✅ | Display name shown in Studio and APIs | | **isDefault** | `boolean` | ✅ | Whether this is the default environment for the organization | | **isSystem** | `boolean` | ✅ | Whether this is a system environment (platform infrastructure, not user data) | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Plan tier for this environment | +| **plan** | `string` | ✅ | Plan tier for this environment | | **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed' \| 'migrating'>` | ✅ | Environment lifecycle status | | **createdBy** | `string` | ✅ | User ID that created the environment | | **createdAt** | `string` | ✅ | Creation timestamp (ISO-8601) | @@ -197,7 +197,7 @@ Public exposure of this environment artifacts (private | unlisted | public). | **organizationId** | `string` | ✅ | Organization that will own the new environment | | **displayName** | `string` | ✅ | Display name shown in Studio and APIs | | **driver** | `string` | optional | Driver key (defaults to provisioning service config) | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | optional | Plan tier | +| **plan** | `string` | optional | Plan tier | | **storageLimitMb** | `integer` | optional | Storage quota in megabytes | | **isDefault** | `boolean` | optional | Mark as the organization default environment | | **createdBy** | `string` | ✅ | User ID that initiated the provisioning | @@ -233,7 +233,7 @@ Public exposure of this environment artifacts (private | unlisted | public). | **organizationId** | `string` | ✅ | Organization being bootstrapped | | **defaultEnvironmentDisplayName** | `string` | ✅ | Display name for the default environment | | **driver** | `string` | optional | Driver key | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | optional | Plan tier | +| **plan** | `string` | optional | Plan tier | | **storageLimitMb** | `integer` | optional | Storage quota in megabytes | | **createdBy** | `string` | ✅ | User ID that initiated provisioning | | **metadata** | `Record` | optional | Free-form metadata | diff --git a/content/docs/references/cloud/tenant.mdx b/content/docs/references/cloud/tenant.mdx index c3d4769962..e11dbd70e5 100644 --- a/content/docs/references/cloud/tenant.mdx +++ b/content/docs/references/cloud/tenant.mdx @@ -72,7 +72,7 @@ const result = PackageInstallationSchema.parse(data); | :--- | :--- | :--- | :--- | | **organizationId** | `string` | ✅ | Organization ID | | **region** | `string` | optional | Deployment region preference | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | +| **plan** | `string` | ✅ | Tenant plan tier | | **storageLimitMb** | `integer` | optional | Storage limit in megabytes | | **metadata** | `Record` | optional | Custom tenant metadata | @@ -102,7 +102,7 @@ const result = PackageInstallationSchema.parse(data); | **organizationId** | `string` | ✅ | Current organization ID | | **organizationSlug** | `string` | optional | Organization slug | | **databaseUrl** | `string` | ✅ | Tenant database URL | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | +| **plan** | `string` | ✅ | Tenant plan tier | | **metadata** | `Record` | optional | Custom tenant metadata | @@ -121,7 +121,7 @@ const result = PackageInstallationSchema.parse(data); | **authToken** | `string` | ✅ | Encrypted tenant-specific auth token | | **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed'>` | ✅ | Database status | | **region** | `string` | ✅ | Deployment region | -| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` | ✅ | Tenant plan tier | +| **plan** | `string` | ✅ | Tenant plan tier | | **storageLimitMb** | `integer` | ✅ | Storage limit in megabytes | | **createdAt** | `string` | ✅ | Database creation timestamp | | **updatedAt** | `string` | ✅ | Last update timestamp | @@ -158,16 +158,6 @@ const result = PackageInstallationSchema.parse(data); --- -## TenantPlan - -### Allowed Values - -* `free` -* `starter` -* `pro` -* `enterprise` -* `custom` - ---