@secondlayer/shared 2.1.0 → 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/db/index.d.ts +39 -137
- package/dist/src/db/index.js.map +2 -2
- package/dist/src/db/jsonb.d.ts +5 -1
- package/dist/src/db/jsonb.js.map +2 -2
- package/dist/src/db/queries/account-spend-caps.d.ts +379 -0
- package/dist/src/db/queries/account-spend-caps.js +60 -0
- package/dist/src/db/queries/account-spend-caps.js.map +10 -0
- package/dist/src/db/queries/account-usage.d.ts +403 -0
- package/dist/src/db/queries/account-usage.js +222 -0
- package/dist/src/db/queries/account-usage.js.map +11 -0
- package/dist/src/db/queries/accounts.d.ts +41 -115
- package/dist/src/db/queries/accounts.js +15 -1
- package/dist/src/db/queries/accounts.js.map +3 -3
- package/dist/src/db/queries/integrity.d.ts +27 -114
- package/dist/src/db/queries/projects.d.ts +27 -114
- package/dist/src/db/queries/provisioning-audit.d.ts +27 -114
- package/dist/src/db/queries/subgraph-gaps.d.ts +27 -114
- package/dist/src/db/queries/subgraphs.d.ts +27 -115
- package/dist/src/db/queries/subgraphs.js +2 -3
- package/dist/src/db/queries/subgraphs.js.map +4 -4
- package/dist/src/db/queries/{workflows.d.ts → tenant-compute-addons.d.ts} +50 -149
- package/dist/src/db/queries/tenant-compute-addons.js +47 -0
- package/dist/src/db/queries/tenant-compute-addons.js.map +10 -0
- package/dist/src/db/queries/tenants.d.ts +40 -117
- package/dist/src/db/queries/tenants.js +9 -6
- package/dist/src/db/queries/tenants.js.map +3 -3
- package/dist/src/db/queries/usage.d.ts +28 -139
- package/dist/src/db/queries/usage.js +5 -64
- package/dist/src/db/queries/usage.js.map +4 -5
- package/dist/src/db/schema.d.ts +34 -136
- package/dist/src/errors.d.ts +8 -7
- package/dist/src/errors.js +11 -12
- package/dist/src/errors.js.map +3 -3
- package/dist/src/index.d.ts +46 -143
- package/dist/src/index.js +11 -12
- package/dist/src/index.js.map +4 -4
- package/dist/src/node/local-client.d.ts +27 -114
- package/dist/src/pricing.d.ts +20 -1
- package/dist/src/pricing.js +58 -1
- package/dist/src/pricing.js.map +3 -3
- package/migrations/0045_drop_marketplace_columns.ts +47 -0
- package/migrations/0046_tenant_activity_signal.ts +47 -0
- package/migrations/0047_usage_daily_tenant_id.ts +73 -0
- package/migrations/0048_tenant_compute_addons.ts +49 -0
- package/migrations/0049_accounts_stripe_customer_id.ts +30 -0
- package/migrations/0050_account_spend_caps.ts +45 -0
- package/migrations/0051_workflow_ai_usage_daily.ts +40 -0
- package/migrations/0052_sentries.ts +61 -0
- package/migrations/0053_workflow_runtime.ts +88 -0
- package/migrations/0054_accounts_plan_hobby.ts +32 -0
- package/migrations/0055_ai_usage_account_scope.ts +108 -0
- package/migrations/0056_drop_workflow_sentry_residuals.ts +23 -0
- package/package.json +26 -14
- package/dist/src/db/queries/workflows.js +0 -260
- package/dist/src/db/queries/workflows.js.map +0 -12
- package/dist/src/lib/plans.d.ts +0 -9
- package/dist/src/lib/plans.js +0 -37
- package/dist/src/lib/plans.js.map +0 -10
- package/dist/src/schemas/workflows.d.ts +0 -70
- package/dist/src/schemas/workflows.js +0 -43
- package/dist/src/schemas/workflows.js.map +0 -10
|
@@ -60,10 +60,6 @@ interface SubgraphsTable {
|
|
|
60
60
|
handler_code: string | null;
|
|
61
61
|
source_code: string | null;
|
|
62
62
|
project_id: string | null;
|
|
63
|
-
is_public: Generated<boolean>;
|
|
64
|
-
tags: Generated<string[]>;
|
|
65
|
-
description: string | null;
|
|
66
|
-
forked_from_id: string | null;
|
|
67
63
|
created_at: Generated<Date>;
|
|
68
64
|
updated_at: Generated<Date>;
|
|
69
65
|
}
|
|
@@ -98,6 +94,7 @@ interface AccountsTable {
|
|
|
98
94
|
bio: string | null;
|
|
99
95
|
avatar_url: string | null;
|
|
100
96
|
slug: string | null;
|
|
97
|
+
stripe_customer_id: string | null;
|
|
101
98
|
created_at: Generated<Date>;
|
|
102
99
|
}
|
|
103
100
|
interface SessionsTable {
|
|
@@ -123,6 +120,7 @@ interface MagicLinksTable {
|
|
|
123
120
|
}
|
|
124
121
|
interface UsageDailyTable {
|
|
125
122
|
account_id: string;
|
|
123
|
+
tenant_id: string | null;
|
|
126
124
|
date: string;
|
|
127
125
|
api_requests: Generated<number>;
|
|
128
126
|
deliveries: Generated<number>;
|
|
@@ -249,83 +247,6 @@ interface ChatMessagesTable {
|
|
|
249
247
|
metadata: unknown | null;
|
|
250
248
|
created_at: Generated<Date>;
|
|
251
249
|
}
|
|
252
|
-
interface WorkflowDefinitionsTable {
|
|
253
|
-
id: Generated<string>;
|
|
254
|
-
name: string;
|
|
255
|
-
version: Generated<string>;
|
|
256
|
-
status: Generated<string>;
|
|
257
|
-
trigger_type: string;
|
|
258
|
-
trigger_config: unknown;
|
|
259
|
-
handler_path: string;
|
|
260
|
-
source_code: string | null;
|
|
261
|
-
retries_config: unknown | null;
|
|
262
|
-
timeout_ms: number | null;
|
|
263
|
-
api_key_id: string;
|
|
264
|
-
project_id: string | null;
|
|
265
|
-
created_at: Generated<Date>;
|
|
266
|
-
updated_at: Generated<Date>;
|
|
267
|
-
}
|
|
268
|
-
interface WorkflowRunsTable {
|
|
269
|
-
id: Generated<string>;
|
|
270
|
-
definition_id: string;
|
|
271
|
-
status: Generated<string>;
|
|
272
|
-
trigger_type: string;
|
|
273
|
-
trigger_data: unknown | null;
|
|
274
|
-
dedup_key: string | null;
|
|
275
|
-
error: string | null;
|
|
276
|
-
started_at: Date | null;
|
|
277
|
-
completed_at: Date | null;
|
|
278
|
-
duration_ms: number | null;
|
|
279
|
-
total_ai_tokens: Generated<number>;
|
|
280
|
-
created_at: Generated<Date>;
|
|
281
|
-
}
|
|
282
|
-
interface WorkflowStepsTable {
|
|
283
|
-
id: Generated<string>;
|
|
284
|
-
run_id: string;
|
|
285
|
-
step_index: number;
|
|
286
|
-
step_id: string;
|
|
287
|
-
step_type: string;
|
|
288
|
-
status: Generated<string>;
|
|
289
|
-
input: unknown | null;
|
|
290
|
-
output: unknown | null;
|
|
291
|
-
error: string | null;
|
|
292
|
-
retry_count: Generated<number>;
|
|
293
|
-
ai_tokens_used: Generated<number>;
|
|
294
|
-
started_at: Date | null;
|
|
295
|
-
completed_at: Date | null;
|
|
296
|
-
duration_ms: number | null;
|
|
297
|
-
memo_key: string | null;
|
|
298
|
-
parent_step_id: string | null;
|
|
299
|
-
created_at: Generated<Date>;
|
|
300
|
-
}
|
|
301
|
-
interface WorkflowQueueTable {
|
|
302
|
-
id: Generated<string>;
|
|
303
|
-
run_id: string;
|
|
304
|
-
status: Generated<string>;
|
|
305
|
-
attempts: Generated<number>;
|
|
306
|
-
max_attempts: Generated<number>;
|
|
307
|
-
scheduled_for: Generated<Date>;
|
|
308
|
-
locked_at: Date | null;
|
|
309
|
-
locked_by: string | null;
|
|
310
|
-
error: string | null;
|
|
311
|
-
created_at: Generated<Date>;
|
|
312
|
-
completed_at: Date | null;
|
|
313
|
-
}
|
|
314
|
-
interface WorkflowSchedulesTable {
|
|
315
|
-
id: Generated<string>;
|
|
316
|
-
definition_id: string;
|
|
317
|
-
cron_expr: string;
|
|
318
|
-
timezone: Generated<string>;
|
|
319
|
-
next_run_at: Date;
|
|
320
|
-
last_run_at: Date | null;
|
|
321
|
-
enabled: Generated<boolean>;
|
|
322
|
-
created_at: Generated<Date>;
|
|
323
|
-
}
|
|
324
|
-
interface WorkflowCursorsTable {
|
|
325
|
-
name: string;
|
|
326
|
-
block_height: Generated<number>;
|
|
327
|
-
updated_at: Generated<Date>;
|
|
328
|
-
}
|
|
329
250
|
interface Database {
|
|
330
251
|
blocks: BlocksTable;
|
|
331
252
|
transactions: TransactionsTable;
|
|
@@ -351,16 +272,10 @@ interface Database {
|
|
|
351
272
|
team_invitations: TeamInvitationsTable;
|
|
352
273
|
chat_sessions: ChatSessionsTable;
|
|
353
274
|
chat_messages: ChatMessagesTable;
|
|
354
|
-
workflow_definitions: WorkflowDefinitionsTable;
|
|
355
|
-
workflow_runs: WorkflowRunsTable;
|
|
356
|
-
workflow_steps: WorkflowStepsTable;
|
|
357
|
-
workflow_queue: WorkflowQueueTable;
|
|
358
|
-
workflow_schedules: WorkflowSchedulesTable;
|
|
359
|
-
workflow_cursors: WorkflowCursorsTable;
|
|
360
|
-
workflow_signer_secrets: WorkflowSignerSecretsTable;
|
|
361
|
-
workflow_budgets: WorkflowBudgetsTable;
|
|
362
275
|
tenants: TenantsTable;
|
|
363
276
|
tenant_usage_monthly: TenantUsageMonthlyTable;
|
|
277
|
+
tenant_compute_addons: TenantComputeAddonsTable;
|
|
278
|
+
account_spend_caps: AccountSpendCapsTable;
|
|
364
279
|
provisioning_audit_log: ProvisioningAuditLogTable;
|
|
365
280
|
}
|
|
366
281
|
type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
|
|
@@ -383,9 +298,9 @@ interface TenantsTable {
|
|
|
383
298
|
service_key_enc: Buffer;
|
|
384
299
|
api_url_internal: string;
|
|
385
300
|
api_url_public: string;
|
|
386
|
-
trial_ends_at: Date;
|
|
387
301
|
suspended_at: Date | null;
|
|
388
302
|
last_health_check_at: Date | null;
|
|
303
|
+
last_active_at: Generated<Date>;
|
|
389
304
|
service_gen: Generated<number>;
|
|
390
305
|
anon_gen: Generated<number>;
|
|
391
306
|
project_id: string | null;
|
|
@@ -403,6 +318,28 @@ interface TenantUsageMonthlyTable {
|
|
|
403
318
|
first_at: Generated<Date>;
|
|
404
319
|
last_at: Generated<Date>;
|
|
405
320
|
}
|
|
321
|
+
interface TenantComputeAddonsTable {
|
|
322
|
+
id: Generated<string>;
|
|
323
|
+
tenant_id: string;
|
|
324
|
+
memory_mb_delta: Generated<number>;
|
|
325
|
+
cpu_delta: Generated<number | string>;
|
|
326
|
+
storage_mb_delta: Generated<number>;
|
|
327
|
+
effective_from: Generated<Date>;
|
|
328
|
+
effective_until: Date | null;
|
|
329
|
+
stripe_subscription_item_id: string | null;
|
|
330
|
+
created_at: Generated<Date>;
|
|
331
|
+
}
|
|
332
|
+
interface AccountSpendCapsTable {
|
|
333
|
+
account_id: string;
|
|
334
|
+
monthly_cap_cents: number | null;
|
|
335
|
+
compute_cap_cents: number | null;
|
|
336
|
+
storage_cap_cents: number | null;
|
|
337
|
+
ai_cap_cents: number | null;
|
|
338
|
+
alert_threshold_pct: Generated<number>;
|
|
339
|
+
alert_sent_at: Date | null;
|
|
340
|
+
frozen_at: Date | null;
|
|
341
|
+
updated_at: Generated<Date>;
|
|
342
|
+
}
|
|
406
343
|
type ProvisioningAuditEvent = "provision.start" | "provision.success" | "provision.failure" | "suspend" | "resume" | "resize" | "keys.rotate" | "bastion.key.upload" | "bastion.key.revoke" | "teardown";
|
|
407
344
|
type ProvisioningAuditStatus = "ok" | "error";
|
|
408
345
|
interface ProvisioningAuditLogTable {
|
|
@@ -417,30 +354,6 @@ interface ProvisioningAuditLogTable {
|
|
|
417
354
|
error: string | null;
|
|
418
355
|
created_at: Generated<Date>;
|
|
419
356
|
}
|
|
420
|
-
interface WorkflowBudgetsTable {
|
|
421
|
-
id: Generated<string>;
|
|
422
|
-
workflow_definition_id: string;
|
|
423
|
-
/** Period key: "daily:YYYY-MM-DD" | "weekly:YYYY-Www" | "per-run:<uuid>". */
|
|
424
|
-
period: string;
|
|
425
|
-
ai_usd_used: Generated<string>;
|
|
426
|
-
ai_tokens_used: Generated<string>;
|
|
427
|
-
chain_microstx_used: Generated<string>;
|
|
428
|
-
chain_tx_count: Generated<number>;
|
|
429
|
-
run_count: Generated<number>;
|
|
430
|
-
step_count: Generated<number>;
|
|
431
|
-
reset_at: Date;
|
|
432
|
-
created_at: Generated<Date>;
|
|
433
|
-
updated_at: Generated<Date>;
|
|
434
|
-
}
|
|
435
|
-
interface WorkflowSignerSecretsTable {
|
|
436
|
-
id: Generated<string>;
|
|
437
|
-
account_id: string;
|
|
438
|
-
name: string;
|
|
439
|
-
/** AES-GCM ciphertext bytes produced by the runner's KMS on write. */
|
|
440
|
-
encrypted_value: Buffer;
|
|
441
|
-
created_at: Generated<Date>;
|
|
442
|
-
updated_at: Generated<Date>;
|
|
443
|
-
}
|
|
444
357
|
/** Matches the NewBlockPayload shape expected by the indexer's /new_block endpoint */
|
|
445
358
|
interface ReplayBlockPayload {
|
|
446
359
|
block_hash: string;
|
package/dist/src/pricing.d.ts
CHANGED
|
@@ -25,4 +25,23 @@ interface TokenUsage {
|
|
|
25
25
|
* display "-" rather than "$0".
|
|
26
26
|
*/
|
|
27
27
|
declare function computeUsdCost(provider: string, modelId: string, usage: TokenUsage): number | null;
|
|
28
|
-
|
|
28
|
+
interface AiCap {
|
|
29
|
+
/** Max step.ai / generateText / generateObject calls per UTC day. */
|
|
30
|
+
evalsPerDay: number;
|
|
31
|
+
/** Stripe meter events emitted for overage past this cap. */
|
|
32
|
+
overageMeterEventName: "ai_evals";
|
|
33
|
+
}
|
|
34
|
+
/** Resolve the daily AI eval cap for a plan. Unknown plans get Hobby's
|
|
35
|
+
* cap so a stray DB value can't accidentally become unlimited. */
|
|
36
|
+
declare function getAiCapForPlan(plan: string): AiCap;
|
|
37
|
+
/** Included compute hours per billing period. Unknown → hobby. */
|
|
38
|
+
declare function getComputeAllowanceHours(plan: string): number;
|
|
39
|
+
/** Included storage bytes. Unknown → hobby. */
|
|
40
|
+
declare function getStorageAllowanceBytes(plan: string): number;
|
|
41
|
+
/** Whether this plan bills storage overage. Hobby has a hard cap
|
|
42
|
+
* (no overage billing); paid tiers bill $2/GB over allowance. */
|
|
43
|
+
declare function hasStorageOverage(plan: string): boolean;
|
|
44
|
+
declare function getBasePriceCents(plan: string): number;
|
|
45
|
+
/** Capitalized display name for a plan tier. */
|
|
46
|
+
declare function getPlanDisplayName(plan: string): string;
|
|
47
|
+
export { hasStorageOverage, getStorageAllowanceBytes, getPlanDisplayName, getComputeAllowanceHours, getBasePriceCents, getAiCapForPlan, computeUsdCost, TokenUsage, ModelPricing, MODEL_PRICING, AiCap };
|
package/dist/src/pricing.js
CHANGED
|
@@ -38,10 +38,67 @@ function computeUsdCost(provider, modelId, usage) {
|
|
|
38
38
|
return null;
|
|
39
39
|
return usage.inputTokens * p.inputPerMTokens / 1e6 + usage.outputTokens * p.outputPerMTokens / 1e6;
|
|
40
40
|
}
|
|
41
|
+
var AI_CAP_UNLIMITED = {
|
|
42
|
+
evalsPerDay: Number.POSITIVE_INFINITY,
|
|
43
|
+
overageMeterEventName: "ai_evals"
|
|
44
|
+
};
|
|
45
|
+
var AI_CAPS_BY_PLAN = {
|
|
46
|
+
hobby: { evalsPerDay: 50, overageMeterEventName: "ai_evals" },
|
|
47
|
+
launch: { evalsPerDay: 500, overageMeterEventName: "ai_evals" },
|
|
48
|
+
grow: { evalsPerDay: 1000, overageMeterEventName: "ai_evals" },
|
|
49
|
+
scale: { evalsPerDay: 2500, overageMeterEventName: "ai_evals" },
|
|
50
|
+
enterprise: AI_CAP_UNLIMITED
|
|
51
|
+
};
|
|
52
|
+
function getAiCapForPlan(plan) {
|
|
53
|
+
return AI_CAPS_BY_PLAN[plan] ?? AI_CAPS_BY_PLAN.hobby;
|
|
54
|
+
}
|
|
55
|
+
var BYTES_PER_GB = 1024 ** 3;
|
|
56
|
+
var COMPUTE_ALLOWANCE_BY_PLAN = {
|
|
57
|
+
hobby: Number.POSITIVE_INFINITY,
|
|
58
|
+
launch: 500,
|
|
59
|
+
grow: 1000,
|
|
60
|
+
scale: 2500,
|
|
61
|
+
enterprise: Number.POSITIVE_INFINITY
|
|
62
|
+
};
|
|
63
|
+
var STORAGE_ALLOWANCE_BYTES_BY_PLAN = {
|
|
64
|
+
hobby: 5 * BYTES_PER_GB,
|
|
65
|
+
launch: 50 * BYTES_PER_GB,
|
|
66
|
+
grow: 200 * BYTES_PER_GB,
|
|
67
|
+
scale: 1000 * BYTES_PER_GB,
|
|
68
|
+
enterprise: Number.POSITIVE_INFINITY
|
|
69
|
+
};
|
|
70
|
+
function getComputeAllowanceHours(plan) {
|
|
71
|
+
return COMPUTE_ALLOWANCE_BY_PLAN[plan] ?? COMPUTE_ALLOWANCE_BY_PLAN.hobby;
|
|
72
|
+
}
|
|
73
|
+
function getStorageAllowanceBytes(plan) {
|
|
74
|
+
return STORAGE_ALLOWANCE_BYTES_BY_PLAN[plan] ?? STORAGE_ALLOWANCE_BYTES_BY_PLAN.hobby;
|
|
75
|
+
}
|
|
76
|
+
function hasStorageOverage(plan) {
|
|
77
|
+
return plan !== "hobby";
|
|
78
|
+
}
|
|
79
|
+
var BASE_PRICE_CENTS_BY_PLAN = {
|
|
80
|
+
hobby: 0,
|
|
81
|
+
launch: 14900,
|
|
82
|
+
grow: 34900,
|
|
83
|
+
scale: 79900,
|
|
84
|
+
enterprise: 0
|
|
85
|
+
};
|
|
86
|
+
function getBasePriceCents(plan) {
|
|
87
|
+
return BASE_PRICE_CENTS_BY_PLAN[plan] ?? 0;
|
|
88
|
+
}
|
|
89
|
+
function getPlanDisplayName(plan) {
|
|
90
|
+
return plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
91
|
+
}
|
|
41
92
|
export {
|
|
93
|
+
hasStorageOverage,
|
|
94
|
+
getStorageAllowanceBytes,
|
|
95
|
+
getPlanDisplayName,
|
|
96
|
+
getComputeAllowanceHours,
|
|
97
|
+
getBasePriceCents,
|
|
98
|
+
getAiCapForPlan,
|
|
42
99
|
computeUsdCost,
|
|
43
100
|
MODEL_PRICING
|
|
44
101
|
};
|
|
45
102
|
|
|
46
|
-
//# debugId=
|
|
103
|
+
//# debugId=E4067B99B07E03FD64756E2164756E21
|
|
47
104
|
//# sourceMappingURL=pricing.js.map
|
package/dist/src/pricing.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/pricing.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Token → USD pricing constants for internal observability.\n *\n * **Not customer billing.** Tier pricing covers compute; these constants\n * let the dashboard display \"~$X spent in AI today\" and let us track\n * gross-margin internally. Update manually when providers change prices\n * (roughly twice per year).\n *\n * Source: public provider pricing pages as of 2026-04-17.\n */\n\nexport interface ModelPricing {\n\t/** USD per 1M input tokens */\n\tinputPerMTokens: number;\n\t/** USD per 1M output tokens */\n\toutputPerMTokens: number;\n}\n\nexport const MODEL_PRICING: Record<string, Record<string, ModelPricing>> = {\n\tanthropic: {\n\t\t\"claude-haiku-4-5\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-haiku-4-5-20251001\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-sonnet-4-6\": { inputPerMTokens: 3, outputPerMTokens: 15 },\n\t\t\"claude-opus-4-7\": { inputPerMTokens: 15, outputPerMTokens: 75 },\n\t},\n\topenai: {\n\t\t\"gpt-4.1\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o-mini\": { inputPerMTokens: 0.15, outputPerMTokens: 0.6 },\n\t},\n\tgoogle: {\n\t\t\"gemini-2.5-pro\": { inputPerMTokens: 1.25, outputPerMTokens: 10 },\n\t\t\"gemini-2.5-flash\": { inputPerMTokens: 0.3, outputPerMTokens: 2.5 },\n\t},\n};\n\nexport interface TokenUsage {\n\tinputTokens: number;\n\toutputTokens: number;\n}\n\n/**\n * Compute approximate USD cost for an AI step. Returns `null` if the\n * (provider, model) pair isn't in the pricing table — the caller should\n * display \"-\" rather than \"$0\".\n */\nexport function computeUsdCost(\n\tprovider: string,\n\tmodelId: string,\n\tusage: TokenUsage,\n): number | null {\n\tconst p = MODEL_PRICING[provider]?.[modelId];\n\tif (!p) return null;\n\treturn (\n\t\t(usage.inputTokens * p.inputPerMTokens) / 1_000_000 +\n\t\t(usage.outputTokens * p.outputPerMTokens) / 1_000_000\n\t);\n}\n"
|
|
5
|
+
"/**\n * Token → USD pricing constants for internal observability.\n *\n * **Not customer billing.** Tier pricing covers compute; these constants\n * let the dashboard display \"~$X spent in AI today\" and let us track\n * gross-margin internally. Update manually when providers change prices\n * (roughly twice per year).\n *\n * Source: public provider pricing pages as of 2026-04-17.\n */\n\nexport interface ModelPricing {\n\t/** USD per 1M input tokens */\n\tinputPerMTokens: number;\n\t/** USD per 1M output tokens */\n\toutputPerMTokens: number;\n}\n\nexport const MODEL_PRICING: Record<string, Record<string, ModelPricing>> = {\n\tanthropic: {\n\t\t\"claude-haiku-4-5\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-haiku-4-5-20251001\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-sonnet-4-6\": { inputPerMTokens: 3, outputPerMTokens: 15 },\n\t\t\"claude-opus-4-7\": { inputPerMTokens: 15, outputPerMTokens: 75 },\n\t},\n\topenai: {\n\t\t\"gpt-4.1\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o-mini\": { inputPerMTokens: 0.15, outputPerMTokens: 0.6 },\n\t},\n\tgoogle: {\n\t\t\"gemini-2.5-pro\": { inputPerMTokens: 1.25, outputPerMTokens: 10 },\n\t\t\"gemini-2.5-flash\": { inputPerMTokens: 0.3, outputPerMTokens: 2.5 },\n\t},\n};\n\nexport interface TokenUsage {\n\tinputTokens: number;\n\toutputTokens: number;\n}\n\n/**\n * Compute approximate USD cost for an AI step. Returns `null` if the\n * (provider, model) pair isn't in the pricing table — the caller should\n * display \"-\" rather than \"$0\".\n */\nexport function computeUsdCost(\n\tprovider: string,\n\tmodelId: string,\n\tusage: TokenUsage,\n): number | null {\n\tconst p = MODEL_PRICING[provider]?.[modelId];\n\tif (!p) return null;\n\treturn (\n\t\t(usage.inputTokens * p.inputPerMTokens) / 1_000_000 +\n\t\t(usage.outputTokens * p.outputPerMTokens) / 1_000_000\n\t);\n}\n\n// ── Per-tier AI eval caps (workflow-runner) ───────────────────────────\n//\n// Daily `step.ai` budget by tier. Hit the cap → runner throws\n// AI_CAP_REACHED and the step fails cleanly so condition-only paths\n// continue. Overage (past cap) bills to the `ai_evals` Stripe meter on\n// paid tiers.\n//\n// Caps set conservatively so Pro Micro stays margin-positive at\n// realistic AI utilization. Raise based on data, not aspiration.\n\nexport interface AiCap {\n\t/** Max step.ai / generateText / generateObject calls per UTC day. */\n\tevalsPerDay: number;\n\t/** Stripe meter events emitted for overage past this cap. */\n\toverageMeterEventName: \"ai_evals\";\n}\n\nconst AI_CAP_UNLIMITED: AiCap = {\n\tevalsPerDay: Number.POSITIVE_INFINITY,\n\toverageMeterEventName: \"ai_evals\",\n};\n\nconst AI_CAPS_BY_PLAN: Record<string, AiCap> = {\n\thobby: { evalsPerDay: 50, overageMeterEventName: \"ai_evals\" },\n\tlaunch: { evalsPerDay: 500, overageMeterEventName: \"ai_evals\" },\n\tgrow: { evalsPerDay: 1000, overageMeterEventName: \"ai_evals\" },\n\tscale: { evalsPerDay: 2500, overageMeterEventName: \"ai_evals\" },\n\tenterprise: AI_CAP_UNLIMITED,\n};\n\n/** Resolve the daily AI eval cap for a plan. Unknown plans get Hobby's\n * cap so a stray DB value can't accidentally become unlimited. */\nexport function getAiCapForPlan(plan: string): AiCap {\n\treturn AI_CAPS_BY_PLAN[plan] ?? AI_CAPS_BY_PLAN.hobby;\n}\n\n// ── Per-tier compute + storage allowances (usage page) ──────────────\n//\n// \"Included\" amounts per billing period. Actual billing comes from\n// Stripe compute-hours + storage-overage meters; these constants power\n// the dashboard display and the approximation used by `/api/accounts/usage`.\n//\n// Values picked to match `project_supabase_pricing_model.md`:\n// Hobby — 50 h / 5 GB (Nano — free tier)\n// Launch — 500 h / 50 GB ($149/mo)\n// Grow — 1,000 h / 200 GB ($349/mo)\n// Scale — 2,500 h / 1 TB ($799/mo)\n// Enterprise — ∞ / ∞\n\nconst BYTES_PER_GB = 1024 ** 3;\n\n// Hobby has no compute-hour cap — auto-pause after 7d idle is the cap.\n// Paid tiers bill Stripe-metered hours past the included credit; the\n// hour equivalents below are approximate display values. Real billing\n// uses the `compute_hours` meter in Stripe, not these numbers.\nconst COMPUTE_ALLOWANCE_BY_PLAN: Record<string, number> = {\n\thobby: Number.POSITIVE_INFINITY,\n\tlaunch: 500,\n\tgrow: 1000,\n\tscale: 2500,\n\tenterprise: Number.POSITIVE_INFINITY,\n};\n\nconst STORAGE_ALLOWANCE_BYTES_BY_PLAN: Record<string, number> = {\n\thobby: 5 * BYTES_PER_GB,\n\tlaunch: 50 * BYTES_PER_GB,\n\tgrow: 200 * BYTES_PER_GB,\n\tscale: 1000 * BYTES_PER_GB,\n\tenterprise: Number.POSITIVE_INFINITY,\n};\n\n/** Included compute hours per billing period. Unknown → hobby. */\nexport function getComputeAllowanceHours(plan: string): number {\n\treturn COMPUTE_ALLOWANCE_BY_PLAN[plan] ?? COMPUTE_ALLOWANCE_BY_PLAN.hobby;\n}\n\n/** Included storage bytes. Unknown → hobby. */\nexport function getStorageAllowanceBytes(plan: string): number {\n\treturn (\n\t\tSTORAGE_ALLOWANCE_BYTES_BY_PLAN[plan] ??\n\t\tSTORAGE_ALLOWANCE_BYTES_BY_PLAN.hobby\n\t);\n}\n\n/** Whether this plan bills storage overage. Hobby has a hard cap\n * (no overage billing); paid tiers bill $2/GB over allowance. */\nexport function hasStorageOverage(plan: string): boolean {\n\treturn plan !== \"hobby\";\n}\n\n/** Base monthly price for a plan, in cents. Enterprise is custom → 0. */\nconst BASE_PRICE_CENTS_BY_PLAN: Record<string, number> = {\n\thobby: 0,\n\tlaunch: 14900,\n\tgrow: 34900,\n\tscale: 79900,\n\tenterprise: 0,\n};\n\nexport function getBasePriceCents(plan: string): number {\n\treturn BASE_PRICE_CENTS_BY_PLAN[plan] ?? 0;\n}\n\n/** Capitalized display name for a plan tier. */\nexport function getPlanDisplayName(plan: string): string {\n\treturn plan.charAt(0).toUpperCase() + plan.slice(1);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAkBO,IAAM,gBAA8D;AAAA,EAC1E,WAAW;AAAA,IACV,oBAAoB,EAAE,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,IAC9D,6BAA6B,EAAE,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,IACvE,qBAAqB,EAAE,iBAAiB,GAAG,kBAAkB,GAAG;AAAA,IAChE,mBAAmB,EAAE,iBAAiB,IAAI,kBAAkB,GAAG;AAAA,EAChE;AAAA,EACA,QAAQ;AAAA,IACP,WAAW,EAAE,iBAAiB,KAAK,kBAAkB,GAAG;AAAA,IACxD,UAAU,EAAE,iBAAiB,KAAK,kBAAkB,GAAG;AAAA,IACvD,eAAe,EAAE,iBAAiB,MAAM,kBAAkB,IAAI;AAAA,EAC/D;AAAA,EACA,QAAQ;AAAA,IACP,kBAAkB,EAAE,iBAAiB,MAAM,kBAAkB,GAAG;AAAA,IAChE,oBAAoB,EAAE,iBAAiB,KAAK,kBAAkB,IAAI;AAAA,EACnE;AACD;AAYO,SAAS,cAAc,CAC7B,UACA,SACA,OACgB;AAAA,EAChB,MAAM,IAAI,cAAc,YAAY;AAAA,EACpC,IAAI,CAAC;AAAA,IAAG,OAAO;AAAA,EACf,OACE,MAAM,cAAc,EAAE,kBAAmB,MACzC,MAAM,eAAe,EAAE,mBAAoB;AAAA;",
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAkBO,IAAM,gBAA8D;AAAA,EAC1E,WAAW;AAAA,IACV,oBAAoB,EAAE,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,IAC9D,6BAA6B,EAAE,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,IACvE,qBAAqB,EAAE,iBAAiB,GAAG,kBAAkB,GAAG;AAAA,IAChE,mBAAmB,EAAE,iBAAiB,IAAI,kBAAkB,GAAG;AAAA,EAChE;AAAA,EACA,QAAQ;AAAA,IACP,WAAW,EAAE,iBAAiB,KAAK,kBAAkB,GAAG;AAAA,IACxD,UAAU,EAAE,iBAAiB,KAAK,kBAAkB,GAAG;AAAA,IACvD,eAAe,EAAE,iBAAiB,MAAM,kBAAkB,IAAI;AAAA,EAC/D;AAAA,EACA,QAAQ;AAAA,IACP,kBAAkB,EAAE,iBAAiB,MAAM,kBAAkB,GAAG;AAAA,IAChE,oBAAoB,EAAE,iBAAiB,KAAK,kBAAkB,IAAI;AAAA,EACnE;AACD;AAYO,SAAS,cAAc,CAC7B,UACA,SACA,OACgB;AAAA,EAChB,MAAM,IAAI,cAAc,YAAY;AAAA,EACpC,IAAI,CAAC;AAAA,IAAG,OAAO;AAAA,EACf,OACE,MAAM,cAAc,EAAE,kBAAmB,MACzC,MAAM,eAAe,EAAE,mBAAoB;AAAA;AAqB9C,IAAM,mBAA0B;AAAA,EAC/B,aAAa,OAAO;AAAA,EACpB,uBAAuB;AACxB;AAEA,IAAM,kBAAyC;AAAA,EAC9C,OAAO,EAAE,aAAa,IAAI,uBAAuB,WAAW;AAAA,EAC5D,QAAQ,EAAE,aAAa,KAAK,uBAAuB,WAAW;AAAA,EAC9D,MAAM,EAAE,aAAa,MAAM,uBAAuB,WAAW;AAAA,EAC7D,OAAO,EAAE,aAAa,MAAM,uBAAuB,WAAW;AAAA,EAC9D,YAAY;AACb;AAIO,SAAS,eAAe,CAAC,MAAqB;AAAA,EACpD,OAAO,gBAAgB,SAAS,gBAAgB;AAAA;AAgBjD,IAAM,eAAe,QAAQ;AAM7B,IAAM,4BAAoD;AAAA,EACzD,OAAO,OAAO;AAAA,EACd,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,YAAY,OAAO;AACpB;AAEA,IAAM,kCAA0D;AAAA,EAC/D,OAAO,IAAI;AAAA,EACX,QAAQ,KAAK;AAAA,EACb,MAAM,MAAM;AAAA,EACZ,OAAO,OAAO;AAAA,EACd,YAAY,OAAO;AACpB;AAGO,SAAS,wBAAwB,CAAC,MAAsB;AAAA,EAC9D,OAAO,0BAA0B,SAAS,0BAA0B;AAAA;AAI9D,SAAS,wBAAwB,CAAC,MAAsB;AAAA,EAC9D,OACC,gCAAgC,SAChC,gCAAgC;AAAA;AAM3B,SAAS,iBAAiB,CAAC,MAAuB;AAAA,EACxD,OAAO,SAAS;AAAA;AAIjB,IAAM,2BAAmD;AAAA,EACxD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,YAAY;AACb;AAEO,SAAS,iBAAiB,CAAC,MAAsB;AAAA,EACvD,OAAO,yBAAyB,SAAS;AAAA;AAInC,SAAS,kBAAkB,CAAC,MAAsB;AAAA,EACxD,OAAO,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC;AAAA;",
|
|
8
|
+
"debugId": "E4067B99B07E03FD64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drop the marketplace-era columns from `subgraphs`. The marketplace
|
|
5
|
+
* feature is gone (see `0022_marketplace`); these columns were only
|
|
6
|
+
* written by the deleted publish/unpublish endpoints and read by the
|
|
7
|
+
* deleted marketplace browse routes.
|
|
8
|
+
*
|
|
9
|
+
* Production note: the platform DB's `subgraphs` table was manually dropped
|
|
10
|
+
* after migration `0041` as part of the shared→dedicated cutover (see the
|
|
11
|
+
* note there). This migration must tolerate that — it runs fine on OSS
|
|
12
|
+
* deployments and fresh dev DBs where the table still exists, and no-ops
|
|
13
|
+
* on production where the table is already gone.
|
|
14
|
+
*
|
|
15
|
+
* Indexes `subgraphs_is_public_idx` and `subgraphs_tags_idx` from 0022
|
|
16
|
+
* drop automatically with the columns.
|
|
17
|
+
*/
|
|
18
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
19
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
20
|
+
await sql`
|
|
21
|
+
DO $$
|
|
22
|
+
BEGIN
|
|
23
|
+
IF to_regclass('public.subgraphs') IS NOT NULL THEN
|
|
24
|
+
ALTER TABLE subgraphs DROP COLUMN IF EXISTS forked_from_id;
|
|
25
|
+
ALTER TABLE subgraphs DROP COLUMN IF EXISTS description;
|
|
26
|
+
ALTER TABLE subgraphs DROP COLUMN IF EXISTS tags;
|
|
27
|
+
ALTER TABLE subgraphs DROP COLUMN IF EXISTS is_public;
|
|
28
|
+
END IF;
|
|
29
|
+
END$$
|
|
30
|
+
`.execute(db);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
34
|
+
// Re-adding these columns would resurrect dead feature state; only
|
|
35
|
+
// restore them where the table exists, to match the up() guard.
|
|
36
|
+
await sql`
|
|
37
|
+
DO $$
|
|
38
|
+
BEGIN
|
|
39
|
+
IF to_regclass('public.subgraphs') IS NOT NULL THEN
|
|
40
|
+
ALTER TABLE subgraphs ADD COLUMN IF NOT EXISTS is_public boolean NOT NULL DEFAULT false;
|
|
41
|
+
ALTER TABLE subgraphs ADD COLUMN IF NOT EXISTS tags text[] NOT NULL DEFAULT '{}';
|
|
42
|
+
ALTER TABLE subgraphs ADD COLUMN IF NOT EXISTS description text;
|
|
43
|
+
ALTER TABLE subgraphs ADD COLUMN IF NOT EXISTS forked_from_id uuid REFERENCES subgraphs(id) ON DELETE SET NULL;
|
|
44
|
+
END IF;
|
|
45
|
+
END$$
|
|
46
|
+
`.execute(db);
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Switch tenant suspension from time-based (14-day trial) to activity-based
|
|
5
|
+
* (Supabase-style auto-pause on the Hobby tier).
|
|
6
|
+
*
|
|
7
|
+
* - Drop `tenants.trial_ends_at` + its supporting index. The trial model
|
|
8
|
+
* is gone; no grandfathering (pre-launch, zero external users).
|
|
9
|
+
* - Add `tenants.last_active_at timestamptz NOT NULL DEFAULT now()`. Bumped
|
|
10
|
+
* by tenant API middleware on 2xx responses + workflow-runner on run
|
|
11
|
+
* start. The new `tenant-idle-pause` cron suspends Hobby tenants idle
|
|
12
|
+
* beyond a threshold.
|
|
13
|
+
* - Add `tenants_last_active_idx` for the cron's WHERE clause (plan +
|
|
14
|
+
* last_active_at).
|
|
15
|
+
*/
|
|
16
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
17
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
18
|
+
await sql`DROP INDEX IF EXISTS tenants_trial_ends_idx`.execute(db);
|
|
19
|
+
await sql`ALTER TABLE tenants DROP COLUMN IF EXISTS trial_ends_at`.execute(
|
|
20
|
+
db,
|
|
21
|
+
);
|
|
22
|
+
await sql`
|
|
23
|
+
ALTER TABLE tenants
|
|
24
|
+
ADD COLUMN last_active_at timestamptz NOT NULL DEFAULT now()
|
|
25
|
+
`.execute(db);
|
|
26
|
+
await sql`
|
|
27
|
+
CREATE INDEX tenants_last_active_idx
|
|
28
|
+
ON tenants (plan, last_active_at)
|
|
29
|
+
WHERE status = 'active'
|
|
30
|
+
`.execute(db);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
34
|
+
await sql`DROP INDEX IF EXISTS tenants_last_active_idx`.execute(db);
|
|
35
|
+
await sql`ALTER TABLE tenants DROP COLUMN IF EXISTS last_active_at`.execute(
|
|
36
|
+
db,
|
|
37
|
+
);
|
|
38
|
+
await sql`
|
|
39
|
+
ALTER TABLE tenants
|
|
40
|
+
ADD COLUMN trial_ends_at timestamptz NOT NULL DEFAULT (now() + interval '14 days')
|
|
41
|
+
`.execute(db);
|
|
42
|
+
await sql`
|
|
43
|
+
CREATE INDEX tenants_trial_ends_idx
|
|
44
|
+
ON tenants (trial_ends_at)
|
|
45
|
+
WHERE status IN ('provisioning', 'active')
|
|
46
|
+
`.execute(db);
|
|
47
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tenant-scope the `usage_daily` table so Stripe metering (Sprint C) can
|
|
5
|
+
* bill per-tenant, not per-account. Account-level billing worked when
|
|
6
|
+
* accounts were 1:1 with tenants; once we adopt org-level billing where
|
|
7
|
+
* an account owns multiple projects (each = one tenant), we need the
|
|
8
|
+
* tenant dimension.
|
|
9
|
+
*
|
|
10
|
+
* - `tenant_id` added nullable — existing rows predate the column.
|
|
11
|
+
* Backfill is best-effort: rows where the account has exactly one
|
|
12
|
+
* tenant get that tenant's id; ambiguous rows stay NULL.
|
|
13
|
+
* - Unique constraint relaxed from `(account_id, date)` to
|
|
14
|
+
* `(account_id, tenant_id, date)` (NULLs treated as distinct per
|
|
15
|
+
* Postgres default) so future per-tenant rows don't collide with
|
|
16
|
+
* account-level history.
|
|
17
|
+
*/
|
|
18
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
19
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
20
|
+
|
|
21
|
+
await sql`
|
|
22
|
+
ALTER TABLE usage_daily
|
|
23
|
+
ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL
|
|
24
|
+
`.execute(db);
|
|
25
|
+
|
|
26
|
+
// Best-effort backfill — only fill rows where the account has a single
|
|
27
|
+
// tenant. Ambiguous accounts (multi-tenant) stay NULL; Sprint C starts
|
|
28
|
+
// writing tenant_id on every new row.
|
|
29
|
+
await sql`
|
|
30
|
+
UPDATE usage_daily u
|
|
31
|
+
SET tenant_id = t.id
|
|
32
|
+
FROM tenants t
|
|
33
|
+
WHERE u.tenant_id IS NULL
|
|
34
|
+
AND t.account_id = u.account_id
|
|
35
|
+
AND t.status <> 'deleted'
|
|
36
|
+
AND NOT EXISTS (
|
|
37
|
+
SELECT 1 FROM tenants t2
|
|
38
|
+
WHERE t2.account_id = u.account_id
|
|
39
|
+
AND t2.id <> t.id
|
|
40
|
+
AND t2.status <> 'deleted'
|
|
41
|
+
)
|
|
42
|
+
`.execute(db);
|
|
43
|
+
|
|
44
|
+
// Drop old PK/unique so we can widen to include tenant_id.
|
|
45
|
+
await sql`
|
|
46
|
+
ALTER TABLE usage_daily DROP CONSTRAINT IF EXISTS usage_daily_pkey
|
|
47
|
+
`.execute(db);
|
|
48
|
+
await sql`
|
|
49
|
+
ALTER TABLE usage_daily
|
|
50
|
+
ADD CONSTRAINT usage_daily_pkey
|
|
51
|
+
UNIQUE (account_id, tenant_id, date)
|
|
52
|
+
`.execute(db);
|
|
53
|
+
|
|
54
|
+
await sql`
|
|
55
|
+
CREATE INDEX IF NOT EXISTS usage_daily_tenant_date_idx
|
|
56
|
+
ON usage_daily (tenant_id, date DESC)
|
|
57
|
+
`.execute(db);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
61
|
+
await sql`DROP INDEX IF EXISTS usage_daily_tenant_date_idx`.execute(db);
|
|
62
|
+
await sql`
|
|
63
|
+
ALTER TABLE usage_daily DROP CONSTRAINT IF EXISTS usage_daily_pkey
|
|
64
|
+
`.execute(db);
|
|
65
|
+
await sql`
|
|
66
|
+
ALTER TABLE usage_daily
|
|
67
|
+
ADD CONSTRAINT usage_daily_pkey
|
|
68
|
+
PRIMARY KEY (account_id, date)
|
|
69
|
+
`.execute(db);
|
|
70
|
+
await sql`ALTER TABLE usage_daily DROP COLUMN IF EXISTS tenant_id`.execute(
|
|
71
|
+
db,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute add-ons on top of a plan's base spec.
|
|
5
|
+
*
|
|
6
|
+
* The two-axis model (plan × compute) needs somewhere to record "Pro +
|
|
7
|
+
* 4 GB RAM bundle" without mutating the plan. Each row is one add-on;
|
|
8
|
+
* SUM() over active rows for a tenant gives the delta to apply on top
|
|
9
|
+
* of `plans.ts` base compute.
|
|
10
|
+
*
|
|
11
|
+
* Columns kept as *_delta so "zero add-ons" is the identity (no
|
|
12
|
+
* nullables in the sum path). Effective compute = plan base +
|
|
13
|
+
* SUM(active deltas).
|
|
14
|
+
*
|
|
15
|
+
* `effective_until` nullable — open-ended add-on (active until cancelled).
|
|
16
|
+
* Ranges allow future-dated add-ons + mid-cycle cancels cleanly.
|
|
17
|
+
*
|
|
18
|
+
* `stripe_subscription_item_id` nullable — present once Sprint C.2/C.3
|
|
19
|
+
* wire Stripe. For Sprint C.1 the table exists but nothing writes it.
|
|
20
|
+
*/
|
|
21
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
22
|
+
await sql`
|
|
23
|
+
CREATE TABLE tenant_compute_addons (
|
|
24
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
25
|
+
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
26
|
+
memory_mb_delta integer NOT NULL DEFAULT 0,
|
|
27
|
+
cpu_delta numeric(4,2) NOT NULL DEFAULT 0,
|
|
28
|
+
storage_mb_delta integer NOT NULL DEFAULT 0,
|
|
29
|
+
effective_from timestamptz NOT NULL DEFAULT now(),
|
|
30
|
+
effective_until timestamptz,
|
|
31
|
+
stripe_subscription_item_id text,
|
|
32
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
33
|
+
)
|
|
34
|
+
`.execute(db);
|
|
35
|
+
|
|
36
|
+
// Partial index limited to open-ended add-ons (the common case) —
|
|
37
|
+
// `now()` isn't immutable so it can't appear in a WHERE clause here.
|
|
38
|
+
// Closed-end add-ons still get served from a seq scan + date filter,
|
|
39
|
+
// which is cheap at our row counts.
|
|
40
|
+
await sql`
|
|
41
|
+
CREATE INDEX tenant_compute_addons_tenant_idx
|
|
42
|
+
ON tenant_compute_addons (tenant_id)
|
|
43
|
+
WHERE effective_until IS NULL
|
|
44
|
+
`.execute(db);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
48
|
+
await sql`DROP TABLE IF EXISTS tenant_compute_addons`.execute(db);
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Link accounts to Stripe customers.
|
|
5
|
+
*
|
|
6
|
+
* Nullable because we create the Stripe customer lazily — only when an
|
|
7
|
+
* account upgrades past Hobby. Hobby users never show up in Stripe, which
|
|
8
|
+
* keeps the dashboard clean and avoids Stripe-side overhead per free user.
|
|
9
|
+
*
|
|
10
|
+
* Unique index (not constraint) so the column stays nullable but still
|
|
11
|
+
* enforces one-to-one when set.
|
|
12
|
+
*/
|
|
13
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
14
|
+
await sql`
|
|
15
|
+
ALTER TABLE accounts
|
|
16
|
+
ADD COLUMN IF NOT EXISTS stripe_customer_id text
|
|
17
|
+
`.execute(db);
|
|
18
|
+
await sql`
|
|
19
|
+
CREATE UNIQUE INDEX IF NOT EXISTS accounts_stripe_customer_idx
|
|
20
|
+
ON accounts (stripe_customer_id)
|
|
21
|
+
WHERE stripe_customer_id IS NOT NULL
|
|
22
|
+
`.execute(db);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
26
|
+
await sql`DROP INDEX IF EXISTS accounts_stripe_customer_idx`.execute(db);
|
|
27
|
+
await sql`
|
|
28
|
+
ALTER TABLE accounts DROP COLUMN IF EXISTS stripe_customer_id
|
|
29
|
+
`.execute(db);
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-account spending caps + threshold alert state.
|
|
5
|
+
*
|
|
6
|
+
* This is the anti-Supabase-#1-complaint differentiator: soft caps with
|
|
7
|
+
* 80% threshold email alerts, per-line sub-caps, and a clear "frozen"
|
|
8
|
+
* state the user can unfreeze by raising the cap (instead of Supabase's
|
|
9
|
+
* binary cap that blocks certain line items but not compute, producing
|
|
10
|
+
* surprise bills).
|
|
11
|
+
*
|
|
12
|
+
* One row per account (account_id is PK). Null caps mean "no cap" for
|
|
13
|
+
* that line. `frozen_at` is set by the metering worker when a cap is
|
|
14
|
+
* hit; cleared on the next billing cycle's `invoice.paid` webhook. While
|
|
15
|
+
* frozen, meter events stop accumulating for that account.
|
|
16
|
+
*
|
|
17
|
+
* `alert_threshold_pct` default 80 — the Supabase request that's been
|
|
18
|
+
* open since 2023.
|
|
19
|
+
*/
|
|
20
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
21
|
+
await sql`
|
|
22
|
+
CREATE TABLE account_spend_caps (
|
|
23
|
+
account_id uuid PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
|
|
24
|
+
monthly_cap_cents integer,
|
|
25
|
+
compute_cap_cents integer,
|
|
26
|
+
storage_cap_cents integer,
|
|
27
|
+
ai_cap_cents integer,
|
|
28
|
+
alert_threshold_pct integer NOT NULL DEFAULT 80,
|
|
29
|
+
alert_sent_at timestamptz,
|
|
30
|
+
frozen_at timestamptz,
|
|
31
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
32
|
+
)
|
|
33
|
+
`.execute(db);
|
|
34
|
+
|
|
35
|
+
// Fast lookup for the metering crons: "is this account currently frozen?"
|
|
36
|
+
await sql`
|
|
37
|
+
CREATE INDEX account_spend_caps_frozen_idx
|
|
38
|
+
ON account_spend_caps (account_id)
|
|
39
|
+
WHERE frozen_at IS NOT NULL
|
|
40
|
+
`.execute(db);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
44
|
+
await sql`DROP TABLE IF EXISTS account_spend_caps`.execute(db);
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-tenant-per-day AI eval counter.
|
|
5
|
+
*
|
|
6
|
+
* The workflow-runner bumps this on every `step.ai` / `step.generateText`
|
|
7
|
+
* / `step.generateObject` call. A daily per-tier cap (see `pricing.ts`
|
|
8
|
+
* helpers) gates new AI calls — when the cap is hit, `step.ai` throws
|
|
9
|
+
* and workflows degrade to their condition-only path.
|
|
10
|
+
*
|
|
11
|
+
* The runner package is currently dead (tables dropped in migration
|
|
12
|
+
* 0038); when it returns, these rows are the source of truth for cap
|
|
13
|
+
* enforcement + AI overage metering to Stripe.
|
|
14
|
+
*
|
|
15
|
+
* PK on (tenant_id, day) — one row per tenant per UTC day. `evals` is
|
|
16
|
+
* the call count, `cost_usd_cents` is our internal cost estimate for
|
|
17
|
+
* sanity-checking Stripe meter events later.
|
|
18
|
+
*/
|
|
19
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
20
|
+
await sql`
|
|
21
|
+
CREATE TABLE workflow_ai_usage_daily (
|
|
22
|
+
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
23
|
+
day date NOT NULL,
|
|
24
|
+
evals integer NOT NULL DEFAULT 0,
|
|
25
|
+
cost_usd_cents integer NOT NULL DEFAULT 0,
|
|
26
|
+
first_at timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
last_at timestamptz NOT NULL DEFAULT now(),
|
|
28
|
+
PRIMARY KEY (tenant_id, day)
|
|
29
|
+
)
|
|
30
|
+
`.execute(db);
|
|
31
|
+
|
|
32
|
+
await sql`
|
|
33
|
+
CREATE INDEX workflow_ai_usage_daily_lookup_idx
|
|
34
|
+
ON workflow_ai_usage_daily (tenant_id, day DESC)
|
|
35
|
+
`.execute(db);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
39
|
+
await sql`DROP TABLE IF EXISTS workflow_ai_usage_daily`.execute(db);
|
|
40
|
+
}
|