From 2f80f91c72964688e9b574bc1474c7ba0dbdcdac Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 30 Jun 2026 21:26:52 +0200 Subject: [PATCH] feat(billing): stop ingestion once an org is 3 days overdue and never paid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a dunning enforcement path that hard-stops OTLP + Cloudflare Logpush ingestion for orgs that signed up, never paid an invoice, and are now 3+ days past due. Autumn's `check()` does not block on `past_due` by default and can't express the narrower "3 days AND never paid" policy, so this is implemented as Autumn-idiomatic state replication: a Svix-verified `billing.updated` webhook maintains the overdue clock, a daily reconcile cron promotes overdue-≥3d + never-paid orgs to suspended (and clears them on payment), and the ingest gateway reads the resulting flag and 402s. - db: new `org_billing_suspensions` table (+ incremental migration 0003); `suspended_at IS NOT NULL` is the gateway enforcement flag. - api: `billing-webhook.http.ts` (Web Crypto Svix verification, no new dep), `BillingSuspensionService` + pure `BillingSuspensionPolicy`, daily cron wired via `event.cron` dispatch in worker.ts + alchemy/wrangler crons, new `AUTUMN_WEBHOOK_SECRET` env. Extracted the shared Autumn-call helper into `lib/AutumnClient.ts`; added `invoices` to the domain `BillingCustomer`. - ingest: `ingest_suspended` threaded through `OrgRouting` (1s TTL), a `LEFT JOIN org_billing_suspensions` in the key/connector/routing queries, and a `402 billing_suspended` before the entitlement gate on both paths. Tests: policy boundaries, PGlite reconcile (promote/clear), Svix signature accept/tamper/wrong-secret, gateway suspension propagation. Full api suite (654) + ingest cargo tests (39) + typecheck (24 pkgs) green. Co-Authored-By: Claude Opus 4.8 --- apps/api/alchemy.run.ts | 7 +- apps/api/src/app.ts | 4 + apps/api/src/billing-suspension-runtime.ts | 62 + apps/api/src/lib/AutumnClient.ts | 63 + apps/api/src/lib/Env.ts | 2 + apps/api/src/routes/billing-webhook.http.ts | 223 + apps/api/src/routes/billing-webhook.test.ts | 73 + apps/api/src/routes/billing.http.ts | 57 +- .../services/BillingSuspensionPolicy.test.ts | 105 + .../src/services/BillingSuspensionPolicy.ts | 57 + .../services/BillingSuspensionService.test.ts | 162 + .../src/services/BillingSuspensionService.ts | 207 + apps/api/src/worker.ts | 46 +- apps/api/wrangler.jsonc | 5 +- apps/ingest/src/main.rs | 138 +- .../db/drizzle/0003_lowly_fantastic_four.sql | 10 + packages/db/drizzle/meta/0003_snapshot.json | 5677 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/index.ts | 1 + .../db/src/schema/org-billing-suspensions.ts | 29 + packages/domain/src/http/billing.ts | 12 + 21 files changed, 6877 insertions(+), 70 deletions(-) create mode 100644 apps/api/src/billing-suspension-runtime.ts create mode 100644 apps/api/src/lib/AutumnClient.ts create mode 100644 apps/api/src/routes/billing-webhook.http.ts create mode 100644 apps/api/src/routes/billing-webhook.test.ts create mode 100644 apps/api/src/services/BillingSuspensionPolicy.test.ts create mode 100644 apps/api/src/services/BillingSuspensionPolicy.ts create mode 100644 apps/api/src/services/BillingSuspensionService.test.ts create mode 100644 apps/api/src/services/BillingSuspensionService.ts create mode 100644 packages/db/drizzle/0003_lowly_fantastic_four.sql create mode 100644 packages/db/drizzle/meta/0003_snapshot.json create mode 100644 packages/db/src/schema/org-billing-suspensions.ts diff --git a/apps/api/alchemy.run.ts b/apps/api/alchemy.run.ts index 59439d47..9960205f 100644 --- a/apps/api/alchemy.run.ts +++ b/apps/api/alchemy.run.ts @@ -143,8 +143,10 @@ export const createMapleApi = async ({ stage, domains }: CreateMapleApiOptions) url: true, adopt: true, routes: domains.api ? [{ pattern: `${domains.api}/*`, adopt: true }] : undefined, - // Periodic VCS sync backstop (every 12h) — enqueues a refresh per installation; see worker.ts `scheduled`. - crons: ["0 */12 * * *"], + // Cron schedules — dispatched by `event.cron` in worker.ts `scheduled`: + // - "0 *\/12 * * *": VCS sync backstop (enqueues a refresh per installation). + // - "0 6 * * *": daily billing-suspension reconcile (overdue ≥3d + never-paid → stop ingest). + crons: ["0 */12 * * *", "0 6 * * *"], eventSources: [ { queue: vcsSyncQueue, @@ -195,6 +197,7 @@ export const createMapleApi = async ({ stage, domains }: CreateMapleApiOptions) ...optionalPlain("CLERK_PUBLISHABLE_KEY"), ...optionalSecret("CLERK_JWT_KEY"), ...optionalSecret("AUTUMN_SECRET_KEY"), + ...optionalSecret("AUTUMN_WEBHOOK_SECRET"), ...optionalSecret("SD_INTERNAL_TOKEN"), ...optionalSecret("INTERNAL_SERVICE_TOKEN"), ...optionalPlain("HAZEL_API_BASE_URL"), diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 76f8750b..1136b4d1 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -4,6 +4,7 @@ import { HttpMiddleware, HttpRouter, HttpServerResponse } from "effect/unstable/ import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi" import { McpLive } from "./mcp/app" import { HttpBillingLive, HttpBillingPublicLive } from "./routes/billing.http" +import { BillingWebhookRouter } from "./routes/billing-webhook.http" import { HttpAiTriageLive } from "./routes/ai-triage.http" import { HttpAlertsLive } from "./routes/alerts.http" import { HttpAnomaliesLive } from "./routes/anomalies.http" @@ -36,6 +37,7 @@ import { HttpWarehouseLive } from "./routes/warehouse.http" import { AiTriageService } from "./services/AiTriageService" import { AlertRuntime, AlertsService } from "./services/AlertsService" import { AnomalyDetectionService } from "./services/AnomalyDetectionService" +import { BillingSuspensionService } from "./services/BillingSuspensionService" import { BucketCacheService, EdgeCacheService } from "@maple/query-engine/caching" import { CacheBackendLive } from "./lib/CacheBackendLive" import { ErrorsService } from "./services/ErrorsService" @@ -100,6 +102,7 @@ const CoreServicesLive = Layer.mergeAll( OrgIngestKeysService.layer, OrgClickHouseSettingsService.layer, OrganizationService.layer, + BillingSuspensionService.layer, // Shared with ScrapeTargetsService via layer memoization so the proxy and // the internal target list resolve sub-targets from one discovery cache. PlanetScaleDiscoveryService.layer, @@ -237,6 +240,7 @@ export const AllRoutes = Layer.mergeAll( PrometheusScrapeProxyRouter, ScraperInternalRouter, VcsWebhookRouter, + BillingWebhookRouter, McpLive, HealthRouter, McpGetFallback, diff --git a/apps/api/src/billing-suspension-runtime.ts b/apps/api/src/billing-suspension-runtime.ts new file mode 100644 index 00000000..3e458109 --- /dev/null +++ b/apps/api/src/billing-suspension-runtime.ts @@ -0,0 +1,62 @@ +import * as MapleCloudflareSDK from "@maple-dev/effect-sdk/cloudflare" +import { WorkerConfigProviderLayer, WorkerEnvironment } from "@maple/effect-cloudflare" +import { Cause, Effect, Layer } from "effect" +import { DatabasePgLive } from "./lib/DatabasePgLive" +import { Env } from "./lib/Env" +import { BillingSuspensionService } from "./services/BillingSuspensionService" + +// --------------------------------------------------------------------------- +// Per-invocation runtime for the daily billing-suspension reconcile cron. +// Mirrors `vcs-sync-runtime.ts`: its own light layer graph (NOT the fetch +// path's MainLive) so the cron invocation stays within the startup CPU budget. +// --------------------------------------------------------------------------- + +const telemetry = MapleCloudflareSDK.make({ + serviceName: "maple-api", + serviceNamespace: "backend", + repositoryUrl: "https://github.com/Makisuo/maple", +}) + +export const buildBillingSuspensionLayer = (_env: Record) => { + const ConfigLive = WorkerConfigProviderLayer + const EnvLive = Env.layer.pipe(Layer.provide(ConfigLive)) + const DatabaseLive = DatabasePgLive.pipe(Layer.provide(WorkerEnvironment.layer)) + const Base = Layer.mergeAll(EnvLive, DatabaseLive, WorkerEnvironment.layer) + + const ServiceLive = BillingSuspensionService.layer.pipe(Layer.provide(Base)) + + return ServiceLive.pipe(Layer.provideMerge(telemetry.layer), Layer.provideMerge(ConfigLive)) +} + +export const flushBillingTelemetry = (env: Record) => telemetry.flush(env) + +// The cron program: scan the overdue set, promote/clear per the policy. +export const runBillingSuspensionReconcile = Effect.gen(function* () { + const service = yield* BillingSuspensionService + const result = yield* service.runReconcile() + yield* Effect.annotateCurrentSpan({ + "billing.reconcile.scanned": result.scanned, + "billing.reconcile.suspended": result.suspended, + "billing.reconcile.cleared": result.cleared, + "billing.reconcile.outcome": "completed", + }) + yield* Effect.logInfo("[billing] suspension reconcile tick complete").pipe( + Effect.annotateLogs({ + scanned: result.scanned, + suspended: result.suspended, + cleared: result.cleared, + }), + ) +}).pipe( + // tapCause lets the cause propagate so `withSpan` marks the tick as Error. + Effect.tapCause((cause) => + Effect.annotateCurrentSpan({ "billing.reconcile.outcome": "failed" }).pipe( + Effect.flatMap(() => + Effect.logError("[billing] suspension reconcile tick failed").pipe( + Effect.annotateLogs({ error: Cause.pretty(cause) }), + ), + ), + ), + ), + Effect.withSpan("BillingSuspensionReconcile.tick"), +) diff --git a/apps/api/src/lib/AutumnClient.ts b/apps/api/src/lib/AutumnClient.ts new file mode 100644 index 00000000..ca10269d --- /dev/null +++ b/apps/api/src/lib/AutumnClient.ts @@ -0,0 +1,63 @@ +import { autumnHandler, type CustomerData } from "autumn-js/backend" +import { Effect, Schema } from "effect" +import { BillingUpstreamError } from "@maple/domain/http" + +// Shared, dependency-free primitives for speaking the internal `autumn-js/backend` +// contract. Extracted from billing.http.ts so non-HTTP callers (the billing +// reconcile cron, the Autumn webhook receiver) reuse the exact same call path +// instead of re-implementing it. The HTTP billing group still owns the per-org +// edge cache (`readCustomerCached`) — that stays in billing.http.ts. + +export type AutumnResult = Awaited> + +// `autumnHandler` matches its route by `method` + `path`, always POST against +// `${DEFAULT_PATH_PREFIX}/${route}` (= /api/autumn/) regardless of which +// Maple endpoint fronts it, so every call here speaks that internal contract. +export const AUTUMN_PATH_PREFIX = "/api/autumn" + +export const makeCallAutumn = + (secretKey: string | undefined) => + ( + route: string, + body: unknown, + customerId: string | undefined, + customerData?: CustomerData, + ): Effect.Effect => + secretKey === undefined + ? Effect.fail(new BillingUpstreamError({ message: "Billing is not configured" })) + : Effect.tryPromise({ + try: () => + autumnHandler({ + request: { url: `${AUTUMN_PATH_PREFIX}/${route}`, method: "POST", body }, + customerId, + customerData, + clientOptions: { secretKey }, + }), + catch: (error) => + new BillingUpstreamError({ + message: error instanceof Error ? error.message : String(error), + }), + }) + +// Surface a readable message for a non-2xx Autumn response (it carries a +// `{ message }` / `{ error }` body) so the client error isn't an opaque 502. +const upstreamMessage = (result: AutumnResult): string => { + const body = result.response as { message?: unknown; error?: unknown } | null + const message = body?.message ?? body?.error + return typeof message === "string" ? message : `Billing request failed (${result.statusCode})` +} + +export const ensureOk = (result: AutumnResult): Effect.Effect => + result.statusCode >= 200 && result.statusCode < 300 + ? Effect.succeed(result.response) + : Effect.fail(new BillingUpstreamError({ message: upstreamMessage(result) })) + +export const decodeUpstream = ( + schema: S, + value: unknown, +): Effect.Effect => + Schema.decodeUnknownEffect(schema)(value).pipe( + Effect.mapError( + (error) => new BillingUpstreamError({ message: `Unexpected billing response: ${error}` }), + ), + ) diff --git a/apps/api/src/lib/Env.ts b/apps/api/src/lib/Env.ts index 0ac4aab8..008db1e2 100644 --- a/apps/api/src/lib/Env.ts +++ b/apps/api/src/lib/Env.ts @@ -27,6 +27,7 @@ export interface EnvShape { readonly CLERK_JWT_KEY: Option.Option> readonly MAPLE_ORG_ID_OVERRIDE: Option.Option readonly AUTUMN_SECRET_KEY: Option.Option> + readonly AUTUMN_WEBHOOK_SECRET: Option.Option> readonly SD_INTERNAL_TOKEN: Option.Option> readonly INTERNAL_SERVICE_TOKEN: Option.Option> readonly EMAIL_FROM: string @@ -84,6 +85,7 @@ const envConfig = Config.all({ CLERK_JWT_KEY: optionalRedacted("CLERK_JWT_KEY"), MAPLE_ORG_ID_OVERRIDE: optionalString("MAPLE_ORG_ID_OVERRIDE"), AUTUMN_SECRET_KEY: optionalRedacted("AUTUMN_SECRET_KEY"), + AUTUMN_WEBHOOK_SECRET: optionalRedacted("AUTUMN_WEBHOOK_SECRET"), SD_INTERNAL_TOKEN: optionalRedacted("SD_INTERNAL_TOKEN"), INTERNAL_SERVICE_TOKEN: optionalRedacted("INTERNAL_SERVICE_TOKEN"), EMAIL_FROM: stringWithDefault("EMAIL_FROM", "Maple "), diff --git a/apps/api/src/routes/billing-webhook.http.ts b/apps/api/src/routes/billing-webhook.http.ts new file mode 100644 index 00000000..3380a6ed --- /dev/null +++ b/apps/api/src/routes/billing-webhook.http.ts @@ -0,0 +1,223 @@ +import { HttpRouter, type HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { Effect, Option, Redacted } from "effect" +import { BillingSuspensionService } from "../services/BillingSuspensionService" +import { Env } from "../lib/Env" + +// --------------------------------------------------------------------------- +// Public Autumn webhook receiver (`POST /api/billing/autumn/webhook`). Autumn +// delivers via Svix; we verify the `svix-*` headers against AUTUMN_WEBHOOK_SECRET +// (Web Crypto HMAC-SHA256, mirroring the GitHub webhook verifier) and, for a +// `billing.updated` event, re-derive the customer's overdue state from Autumn +// and reconcile its `org_billing_suspensions` row. NOT behind auth — authenticity +// is the signature. See docs/* and the suspension service for the policy. +// --------------------------------------------------------------------------- + +const ROUTE = "/api/billing/autumn/webhook" + +const textResponse = (body: string, status: number) => HttpServerResponse.text(body, { status }) + +const timingSafeEqual = (a: string, b: string): boolean => { + const ba = Buffer.from(a) + const bb = Buffer.from(b) + if (ba.length !== bb.length) return false + let mismatch = 0 + for (let i = 0; i < ba.length; i += 1) mismatch |= ba[i]! ^ bb[i]! + return mismatch === 0 +} + +// Verify a Svix signature: HMAC-SHA256 over `${id}.${timestamp}.${body}` keyed +// by the base64-decoded secret (the part after the `whsec_` prefix), base64 +// compared against any `v1,` token in the space-separated header. Returns +// false on any crypto failure rather than throwing. +export const verifySvixSignature = (input: { + readonly secret: string + readonly svixId: string + readonly svixTimestamp: string + readonly body: string + readonly signatureHeader: string +}) => + Effect.gen(function* () { + const rawSecret = input.secret.startsWith("whsec_") + ? input.secret.slice("whsec_".length) + : input.secret + const keyBytes = Buffer.from(rawSecret, "base64") + const key = yield* Effect.tryPromise({ + try: () => + crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, [ + "sign", + ]), + catch: () => "import_failed" as const, + }).pipe(Effect.option) + if (Option.isNone(key)) return false + + const signedContent = `${input.svixId}.${input.svixTimestamp}.${input.body}` + const mac = yield* Effect.tryPromise({ + try: () => crypto.subtle.sign("HMAC", key.value, new TextEncoder().encode(signedContent)), + catch: () => "sign_failed" as const, + }).pipe(Effect.option) + if (Option.isNone(mac)) return false + + const expected = Buffer.from(mac.value).toString("base64") + // Header: space-separated tokens like "v1, v1a,". + const candidates = input.signatureHeader.split(" ").map((token) => { + const comma = token.indexOf(",") + return comma === -1 ? token : token.slice(comma + 1) + }) + return candidates.some((candidate) => candidate.length > 0 && timingSafeEqual(expected, candidate)) + }) + +// Pull the org (Autumn customer_id) out of a `billing.updated` payload. Autumn +// nests the changed entity under `data`; tolerate the common id placements so a +// minor shape change doesn't silently drop events. +export const extractCustomerId = (event: unknown): string | null => { + if (typeof event !== "object" || event === null) return null + const data = (event as { data?: unknown }).data + const containers = [data, event].filter( + (value): value is Record => typeof value === "object" && value !== null, + ) + for (const container of containers) { + const direct = container.customer_id ?? container.customerId + if (typeof direct === "string" && direct.length > 0) return direct + const customer = container.customer + if (typeof customer === "object" && customer !== null) { + const nested = (customer as { id?: unknown }).id + if (typeof nested === "string" && nested.length > 0) return nested + } + } + return null +} + +const safeJsonParse = (body: string): unknown => { + try { + return JSON.parse(body) + } catch { + return null + } +} + +export const BillingWebhookRouter = HttpRouter.use((router) => + Effect.gen(function* () { + const env = yield* Env + const service = yield* BillingSuspensionService + + yield* router.add("POST", ROUTE, (req: HttpServerRequest.HttpServerRequest) => + Effect.gen(function* () { + yield* Effect.annotateCurrentSpan({ + "http.request.method": req.method, + "http.route": ROUTE, + }) + const headers = req.headers as Record + + if (Option.isNone(env.AUTUMN_WEBHOOK_SECRET)) { + yield* Effect.logWarning( + "[billing] webhook rejected: AUTUMN_WEBHOOK_SECRET not configured", + ) + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": 401, + "otel.status_code": "Ok", + "billing.webhook.outcome": "rejected", + "billing.webhook.reason": "secret_not_configured", + }) + return textResponse("Webhook secret not configured", 401) + } + + const svixId = headers["svix-id"] + const svixTimestamp = headers["svix-timestamp"] + const svixSignature = headers["svix-signature"] + const bodyOpt = yield* req.text.pipe(Effect.option) + if (Option.isNone(bodyOpt) || bodyOpt.value.length === 0) { + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": 400, + "otel.status_code": "Ok", + "billing.webhook.outcome": "rejected", + "billing.webhook.reason": "empty_body", + }) + return textResponse("Missing request body", 400) + } + if (!svixId || !svixTimestamp || !svixSignature) { + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": 400, + "otel.status_code": "Ok", + "billing.webhook.outcome": "rejected", + "billing.webhook.reason": "missing_headers", + }) + return textResponse("Missing svix signature headers", 400) + } + + const valid = yield* verifySvixSignature({ + secret: Redacted.value(env.AUTUMN_WEBHOOK_SECRET.value), + svixId, + svixTimestamp, + body: bodyOpt.value, + signatureHeader: svixSignature, + }) + if (!valid) { + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": 401, + "otel.status_code": "Ok", + "billing.webhook.outcome": "rejected", + "billing.webhook.reason": "signature_mismatch", + }) + return textResponse("Invalid signature", 401) + } + + const event = safeJsonParse(bodyOpt.value) + const eventType = + typeof event === "object" && event !== null + ? (event as { type?: unknown }).type + : undefined + if (eventType !== "billing.updated") { + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": 200, + "otel.status_code": "Ok", + "billing.webhook.outcome": "ignored", + "billing.webhook.event_type": typeof eventType === "string" ? eventType : "unknown", + }) + return textResponse("ignored", 200) + } + + const orgId = extractCustomerId(event) + if (!orgId) { + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": 200, + "otel.status_code": "Ok", + "billing.webhook.outcome": "ignored", + "billing.webhook.reason": "no_customer_id", + }) + return textResponse("ignored", 200) + } + + // Failure here returns 500 so Svix retries — the row insert must be + // reliable, otherwise the org would never be picked up by the cron. + return yield* service.refreshOverdueState(orgId).pipe( + Effect.flatMap(() => + Effect.annotateCurrentSpan({ + "http.response.status_code": 200, + "otel.status_code": "Ok", + "billing.webhook.outcome": "handled", + "billing.org_id": orgId, + }).pipe(Effect.as(textResponse("ok", 200))), + ), + Effect.catch((error) => + Effect.annotateCurrentSpan({ + "http.response.status_code": 500, + "otel.status_code": "Error", + "billing.webhook.outcome": "failed", + "billing.org_id": orgId, + }).pipe( + Effect.flatMap(() => + Effect.logError("[billing] webhook processing failed").pipe( + Effect.annotateLogs({ + orgId, + error: error instanceof Error ? error.message : String(error), + }), + ), + ), + Effect.as(textResponse("processing failed", 500)), + ), + ), + ) + }).pipe(Effect.withSpan("BillingWebhook.receive")), + ) + }), +) diff --git a/apps/api/src/routes/billing-webhook.test.ts b/apps/api/src/routes/billing-webhook.test.ts new file mode 100644 index 00000000..4ce1ca2a --- /dev/null +++ b/apps/api/src/routes/billing-webhook.test.ts @@ -0,0 +1,73 @@ +import { createHmac } from "node:crypto" +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" +import { extractCustomerId, verifySvixSignature } from "./billing-webhook.http" + +const KEY_BYTES = Buffer.alloc(24, 9) +const SECRET = `whsec_${KEY_BYTES.toString("base64")}` +const SVIX_ID = "msg_123" +const SVIX_TS = "1700000000" + +const sign = (body: string) => + `v1,${createHmac("sha256", KEY_BYTES).update(`${SVIX_ID}.${SVIX_TS}.${body}`).digest("base64")}` + +const verify = (input: { + secret?: string + body: string + signatureHeader: string +}) => + Effect.runPromise( + verifySvixSignature({ + secret: input.secret ?? SECRET, + svixId: SVIX_ID, + svixTimestamp: SVIX_TS, + body: input.body, + signatureHeader: input.signatureHeader, + }), + ) + +describe("verifySvixSignature", () => { + it("accepts a correctly signed payload", async () => { + const body = JSON.stringify({ type: "billing.updated" }) + expect(await verify({ body, signatureHeader: sign(body) })).toBe(true) + }) + + it("accepts when one of several signature tokens matches", async () => { + const body = JSON.stringify({ type: "billing.updated" }) + expect(await verify({ body, signatureHeader: `v1,deadbeef ${sign(body)}` })).toBe(true) + }) + + it("rejects a tampered body", async () => { + const signature = sign(JSON.stringify({ type: "billing.updated" })) + expect(await verify({ body: JSON.stringify({ type: "evil" }), signatureHeader: signature })).toBe( + false, + ) + }) + + it("rejects a signature made with the wrong secret", async () => { + const body = JSON.stringify({ type: "billing.updated" }) + const wrong = `v1,${createHmac("sha256", Buffer.alloc(24, 1)).update(`${SVIX_ID}.${SVIX_TS}.${body}`).digest("base64")}` + expect(await verify({ body, signatureHeader: wrong })).toBe(false) + }) +}) + +describe("extractCustomerId", () => { + it("reads data.customer_id", () => { + expect(extractCustomerId({ type: "billing.updated", data: { customer_id: "org_a" } })).toBe( + "org_a", + ) + }) + + it("reads nested data.customer.id", () => { + expect(extractCustomerId({ data: { customer: { id: "org_b" } } })).toBe("org_b") + }) + + it("reads a top-level customerId", () => { + expect(extractCustomerId({ customerId: "org_c" })).toBe("org_c") + }) + + it("returns null when no customer id is present", () => { + expect(extractCustomerId({ data: {} })).toBeNull() + expect(extractCustomerId(null)).toBeNull() + }) +}) diff --git a/apps/api/src/routes/billing.http.ts b/apps/api/src/routes/billing.http.ts index 3a6e594f..52155bfe 100644 --- a/apps/api/src/routes/billing.http.ts +++ b/apps/api/src/routes/billing.http.ts @@ -1,7 +1,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpServerRequest } from "effect/unstable/http" import { Effect, Option, Redacted, Schema } from "effect" -import { autumnHandler, type CustomerData } from "autumn-js/backend" +import type { CustomerData } from "autumn-js/backend" import { EdgeCacheService, type EdgeCacheServiceShape } from "@maple/query-engine/caching" import { isActivePlanSubscription } from "@maple/domain/billing" import { @@ -16,16 +16,10 @@ import { MapleApi, PreviewAttachResult, } from "@maple/domain/http" +import { type AutumnResult, decodeUpstream, ensureOk, makeCallAutumn } from "../lib/AutumnClient" import { Env } from "../lib/Env" import { AuthService, type AuthServiceShape } from "../services/AuthService" -type AutumnResult = Awaited> - -// `autumnHandler` matches its route by `method` + `path`, always POST against -// `${DEFAULT_PATH_PREFIX}/${route}` (= /api/autumn/) regardless of which -// Maple endpoint fronts it, so every call here speaks that internal contract. -const AUTUMN_PATH_PREFIX = "/api/autumn" - // getOrCreateCustomer fires on every page load (hot path) and its latency is // dominated by the upstream Autumn call. Cache its success response per org for // 5 minutes behind the shared edge cache (single-flight dedup collapses @@ -97,53 +91,6 @@ export const readCustomerCached = ( ), ) -const makeCallAutumn = - (secretKey: string | undefined) => - ( - route: string, - body: unknown, - customerId: string | undefined, - customerData?: CustomerData, - ): Effect.Effect => - secretKey === undefined - ? Effect.fail(new BillingUpstreamError({ message: "Billing is not configured" })) - : Effect.tryPromise({ - try: () => - autumnHandler({ - request: { url: `${AUTUMN_PATH_PREFIX}/${route}`, method: "POST", body }, - customerId, - customerData, - clientOptions: { secretKey }, - }), - catch: (error) => - new BillingUpstreamError({ - message: error instanceof Error ? error.message : String(error), - }), - }) - -// Surface a readable message for a non-2xx Autumn response (it carries a -// `{ message }` / `{ error }` body) so the client error isn't an opaque 502. -const upstreamMessage = (result: AutumnResult): string => { - const body = result.response as { message?: unknown; error?: unknown } | null - const message = body?.message ?? body?.error - return typeof message === "string" ? message : `Billing request failed (${result.statusCode})` -} - -const ensureOk = (result: AutumnResult): Effect.Effect => - result.statusCode >= 200 && result.statusCode < 300 - ? Effect.succeed(result.response) - : Effect.fail(new BillingUpstreamError({ message: upstreamMessage(result) })) - -const decodeUpstream = ( - schema: S, - value: unknown, -): Effect.Effect => - Schema.decodeUnknownEffect(schema)(value).pipe( - Effect.mapError( - (error) => new BillingUpstreamError({ message: `Unexpected billing response: ${error}` }), - ), - ) - // Enrich checkout (attach) with Clerk-resolved identity so the customer is // identified before Stripe and the buyer's email is pre-filled. Ported verbatim // from the retired proxy's ENRICHED_ROUTES handling. diff --git a/apps/api/src/services/BillingSuspensionPolicy.test.ts b/apps/api/src/services/BillingSuspensionPolicy.test.ts new file mode 100644 index 00000000..fb174b9b --- /dev/null +++ b/apps/api/src/services/BillingSuspensionPolicy.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "@effect/vitest" +import { BillingCustomer, BillingInvoice, BillingSubscription } from "@maple/domain/http" +import { + hasNeverPaid, + isPastDue, + OVERDUE_GRACE_MS, + shouldSuspend, +} from "./BillingSuspensionPolicy" + +const NOW = 1_700_000_000_000 + +const sub = (fields: Partial<{ pastDue: boolean; addOn: boolean; status: string }>) => + new BillingSubscription({ + planId: "startup", + status: fields.status ?? "active", + ...(fields.addOn !== undefined ? { addOn: fields.addOn } : {}), + ...(fields.pastDue !== undefined ? { pastDue: fields.pastDue } : {}), + }) + +const customer = (opts: { + subs: ReadonlyArray + invoices?: ReadonlyArray +}) => + new BillingCustomer({ + id: "org_x", + subscriptions: opts.subs, + ...(opts.invoices !== undefined ? { invoices: opts.invoices } : {}), + }) + +const invoice = (status: string) => new BillingInvoice({ stripeId: `in_${status}`, status }) + +describe("isPastDue", () => { + it("is true when a non-add-on subscription is past_due", () => { + expect(isPastDue(customer({ subs: [sub({ pastDue: true })] }))).toBe(true) + }) + + it("ignores add-on subscriptions", () => { + expect(isPastDue(customer({ subs: [sub({ pastDue: true, addOn: true })] }))).toBe(false) + }) + + it("is false when nothing is past_due", () => { + expect(isPastDue(customer({ subs: [sub({ pastDue: false })] }))).toBe(false) + }) +}) + +describe("hasNeverPaid", () => { + it("is true when there are no invoices", () => { + expect(hasNeverPaid(customer({ subs: [sub({})] }))).toBe(true) + }) + + it("is true when no invoice is paid", () => { + expect(hasNeverPaid(customer({ subs: [sub({})], invoices: [invoice("open")] }))).toBe(true) + }) + + it("is false once any invoice is paid", () => { + expect( + hasNeverPaid(customer({ subs: [sub({})], invoices: [invoice("open"), invoice("paid")] })), + ).toBe(false) + }) +}) + +describe("shouldSuspend", () => { + const pastDueNeverPaid = customer({ subs: [sub({ pastDue: true })], invoices: [invoice("open")] }) + + it("suspends a never-paid org overdue past the grace window", () => { + const decision = shouldSuspend({ + customer: pastDueNeverPaid, + overdueSince: NOW - OVERDUE_GRACE_MS - 1, + now: NOW, + }) + expect(decision.suspend).toBe(true) + expect(decision.overdueInvoiceId).toBe("in_open") + }) + + it("does not suspend before the grace window elapses", () => { + const decision = shouldSuspend({ + customer: pastDueNeverPaid, + overdueSince: NOW - 2 * 24 * 60 * 60 * 1000, // 2 days + now: NOW, + }) + expect(decision.suspend).toBe(false) + expect(decision.overdueInvoiceId).toBeNull() + }) + + it("does not suspend an org that has ever paid", () => { + const decision = shouldSuspend({ + customer: customer({ + subs: [sub({ pastDue: true })], + invoices: [invoice("open"), invoice("paid")], + }), + overdueSince: NOW - OVERDUE_GRACE_MS - 1, + now: NOW, + }) + expect(decision.suspend).toBe(false) + }) + + it("does not suspend once the subscription is no longer past_due", () => { + const decision = shouldSuspend({ + customer: customer({ subs: [sub({ pastDue: false })], invoices: [invoice("open")] }), + overdueSince: NOW - OVERDUE_GRACE_MS - 1, + now: NOW, + }) + expect(decision.suspend).toBe(false) + }) +}) diff --git a/apps/api/src/services/BillingSuspensionPolicy.ts b/apps/api/src/services/BillingSuspensionPolicy.ts new file mode 100644 index 00000000..7edc884d --- /dev/null +++ b/apps/api/src/services/BillingSuspensionPolicy.ts @@ -0,0 +1,57 @@ +import type { BillingCustomer } from "@maple/domain/http" + +// Pure decision logic for the "stop ingestion once 3 days overdue and never +// paid" policy. Kept free of Effect/DB so it can be unit-tested directly. + +// The overdue grace window: an org must be past_due for at least this long +// before ingestion is suspended. +export const OVERDUE_GRACE_MS = 3 * 24 * 60 * 60 * 1000 + +// Invoice statuses that count as "still owed" (an unpaid, finalized invoice). +const UNPAID_INVOICE_STATUSES: ReadonlySet = new Set(["open", "uncollectible", "past_due"]) + +// A non-add-on subscription currently flagged past_due by Autumn. `pastDue` is +// Autumn's canonical overdue signal — we don't reconstruct it from raw invoices. +export const isPastDue = (customer: BillingCustomer): boolean => + customer.subscriptions.some((sub) => sub.addOn !== true && sub.pastDue === true) + +// "Never paid" = no settled invoice on record. Requires the customer to have +// been fetched with `expand: ["invoices"]`; an absent/empty invoices array means +// no paid invoice exists, which is exactly the never-converted signup we target. +export const hasNeverPaid = (customer: BillingCustomer): boolean => { + const invoices = customer.invoices ?? [] + return !invoices.some((invoice) => invoice.status === "paid") +} + +// The Stripe id of the first still-owed invoice, for audit on the suspension row. +export const firstUnpaidInvoiceId = (customer: BillingCustomer): string | null => { + const invoices = customer.invoices ?? [] + const owed = invoices.find( + (invoice) => typeof invoice.status === "string" && UNPAID_INVOICE_STATUSES.has(invoice.status), + ) + return owed?.stripeId ?? null +} + +export interface SuspendDecision { + readonly suspend: boolean + readonly overdueInvoiceId: string | null +} + +// Should an already-overdue org be promoted to suspended? True only when it is +// still past_due, has never paid, and has been overdue for >= the grace window. +// `overdueSince` / `now` are epoch ms. +export const shouldSuspend = (input: { + readonly customer: BillingCustomer + readonly overdueSince: number + readonly now: number + readonly graceMs?: number +}): SuspendDecision => { + const graceMs = input.graceMs ?? OVERDUE_GRACE_MS + const elapsed = input.now - input.overdueSince + const suspend = + isPastDue(input.customer) && hasNeverPaid(input.customer) && elapsed >= graceMs + return { + suspend, + overdueInvoiceId: suspend ? firstUnpaidInvoiceId(input.customer) : null, + } +} diff --git a/apps/api/src/services/BillingSuspensionService.test.ts b/apps/api/src/services/BillingSuspensionService.test.ts new file mode 100644 index 00000000..8b2906ea --- /dev/null +++ b/apps/api/src/services/BillingSuspensionService.test.ts @@ -0,0 +1,162 @@ +import { afterEach, describe, expect, it } from "@effect/vitest" +import { BillingCustomer, BillingInvoice, BillingSubscription } from "@maple/domain/http" +import { Effect } from "effect" +import { Database } from "../lib/DatabaseLive" +import { + cleanupTestDbs, + createTestDb, + executeSql, + queryFirstRow, + type TestDb, +} from "../lib/test-pglite" +import { OVERDUE_GRACE_MS } from "./BillingSuspensionPolicy" +import { + applyOverdueState, + type FetchCustomer, + reconcileSuspensions, +} from "./BillingSuspensionService" + +const trackedDbs: TestDb[] = [] +afterEach(() => cleanupTestDbs(trackedDbs)) + +const NOW = 1_700_000_000_000 + +const subscription = (pastDue: boolean) => + new BillingSubscription({ planId: "startup", status: "active", pastDue }) + +const customer = (opts: { pastDue: boolean; paid?: boolean }) => + new BillingCustomer({ + id: "org_x", + subscriptions: [subscription(opts.pastDue)], + invoices: opts.paid + ? [new BillingInvoice({ stripeId: "in_paid", status: "paid" })] + : [new BillingInvoice({ stripeId: "in_open", status: "open" })], + }) + +const fakeFetch = + (byOrg: Record): FetchCustomer => + (orgId) => + Effect.succeed(byOrg[orgId] ?? customer({ pastDue: false })) + +const seedOverdue = (db: TestDb, orgId: string, overdueSinceMs: number, suspendedAtMs?: number) => + executeSql( + db, + `INSERT INTO org_billing_suspensions + (org_id, overdue_since, suspended_at, overdue_invoice_id, reason, created_at, updated_at) + VALUES ($1, $2, $3, NULL, 'unpaid_overdue', $4, $4)`, + [ + orgId, + new Date(overdueSinceMs).toISOString(), + suspendedAtMs === undefined ? null : new Date(suspendedAtMs).toISOString(), + new Date(NOW).toISOString(), + ], + ) + +const readRow = (db: TestDb, orgId: string) => + queryFirstRow<{ org_id: string; suspended_at: string | null; overdue_invoice_id: string | null }>( + db, + "SELECT org_id, suspended_at, overdue_invoice_id FROM org_billing_suspensions WHERE org_id = $1", + [orgId], + ) + +const run = (db: TestDb, program: Effect.Effect) => + Effect.runPromise(program.pipe(Effect.provide(db.layer)) as Effect.Effect) + +// Migrations apply lazily when the Database layer is first built. Force that +// before any raw-SQL seeding so the table exists. +const ensureMigrated = (db: TestDb) => run(db, Effect.void) + +describe("applyOverdueState", () => { + it("inserts an overdue row when the customer is past_due", async () => { + const db = createTestDb(trackedDbs) + await run( + db, + Effect.gen(function* () { + const database = yield* Database + yield* applyOverdueState(database, "org_a", customer({ pastDue: true }), NOW) + }), + ) + const row = await readRow(db, "org_a") + expect(row?.org_id).toBe("org_a") + expect(row?.suspended_at).toBeNull() + }) + + it("clears the row when the customer is no longer past_due", async () => { + const db = createTestDb(trackedDbs) + await ensureMigrated(db) + await seedOverdue(db, "org_a", NOW - OVERDUE_GRACE_MS - 1) + await run( + db, + Effect.gen(function* () { + const database = yield* Database + yield* applyOverdueState(database, "org_a", customer({ pastDue: false }), NOW) + }), + ) + expect(await readRow(db, "org_a")).toBeUndefined() + }) +}) + +describe("reconcileSuspensions", () => { + it("suspends an overdue-≥3d, never-paid org", async () => { + const db = createTestDb(trackedDbs) + await ensureMigrated(db) + await seedOverdue(db, "org_overdue", NOW - OVERDUE_GRACE_MS - 1) + + const result = (await run( + db, + Effect.gen(function* () { + const database = yield* Database + return yield* reconcileSuspensions( + database, + fakeFetch({ org_overdue: customer({ pastDue: true }) }), + NOW, + ) + }), + )) as { scanned: number; suspended: number; cleared: number } + + expect(result).toEqual({ scanned: 1, suspended: 1, cleared: 0 }) + const row = await readRow(db, "org_overdue") + expect(row?.suspended_at).not.toBeNull() + expect(row?.overdue_invoice_id).toBe("in_open") + }) + + it("does not suspend before the grace window elapses", async () => { + const db = createTestDb(trackedDbs) + await ensureMigrated(db) + await seedOverdue(db, "org_recent", NOW - 2 * 24 * 60 * 60 * 1000) + + await run( + db, + Effect.gen(function* () { + const database = yield* Database + return yield* reconcileSuspensions( + database, + fakeFetch({ org_recent: customer({ pastDue: true }) }), + NOW, + ) + }), + ) + expect((await readRow(db, "org_recent"))?.suspended_at).toBeNull() + }) + + it("clears a suspended org once it has paid", async () => { + const db = createTestDb(trackedDbs) + await ensureMigrated(db) + await seedOverdue(db, "org_paid", NOW - OVERDUE_GRACE_MS - 1, NOW - 1000) + + const result = (await run( + db, + Effect.gen(function* () { + const database = yield* Database + return yield* reconcileSuspensions( + database, + fakeFetch({ org_paid: customer({ pastDue: false, paid: true }) }), + NOW, + ) + }), + )) as { scanned: number; suspended: number; cleared: number } + + expect(result.cleared).toBe(1) + expect(await readRow(db, "org_paid")).toBeUndefined() + }) +}) diff --git a/apps/api/src/services/BillingSuspensionService.ts b/apps/api/src/services/BillingSuspensionService.ts new file mode 100644 index 00000000..d6634f2b --- /dev/null +++ b/apps/api/src/services/BillingSuspensionService.ts @@ -0,0 +1,207 @@ +import { BillingCustomer, type BillingUpstreamError } from "@maple/domain/http" +import { orgBillingSuspensions } from "@maple/db" +import { eq } from "drizzle-orm" +import { Clock, Context, Effect, Layer, Option, Redacted } from "effect" +import { decodeUpstream, ensureOk, makeCallAutumn } from "../lib/AutumnClient" +import { Database, type DatabaseError, type DatabaseShape, toDatabaseError } from "../lib/DatabaseLive" +import { Env } from "../lib/Env" +import { isPastDue, shouldSuspend } from "./BillingSuspensionPolicy" + +const SUSPENSION_REASON = "unpaid_overdue" + +export interface ReconcileResult { + readonly scanned: number + readonly suspended: number + readonly cleared: number +} + +// Resolves an org's Autumn customer (optionally with invoices expanded). The +// real implementation calls Autumn; tests inject a fake. +export type FetchCustomer = ( + orgId: string, + expandInvoices: boolean, +) => Effect.Effect + +// --- DB primitives (Database-shape in, no Env/Autumn) ----------------------- + +const upsertOverdue = (database: DatabaseShape, orgId: string, nowMs: number) => + database + .execute((db) => + db + .insert(orgBillingSuspensions) + .values({ + orgId, + overdueSince: new Date(nowMs), + suspendedAt: null, + overdueInvoiceId: null, + reason: SUSPENSION_REASON, + createdAt: new Date(nowMs), + updatedAt: new Date(nowMs), + }) + // Idempotent: an existing row keeps its original overdueSince so the + // 3-day clock isn't reset by repeated past_due webhooks. + .onConflictDoNothing(), + ) + .pipe(Effect.mapError(toDatabaseError)) + +const markSuspended = ( + database: DatabaseShape, + orgId: string, + nowMs: number, + overdueInvoiceId: string | null, +) => + database + .execute((db) => + db + .update(orgBillingSuspensions) + .set({ suspendedAt: new Date(nowMs), overdueInvoiceId, updatedAt: new Date(nowMs) }) + .where(eq(orgBillingSuspensions.orgId, orgId)), + ) + .pipe(Effect.mapError(toDatabaseError)) + +const clearOrg = (database: DatabaseShape, orgId: string) => + database + .execute((db) => + db.delete(orgBillingSuspensions).where(eq(orgBillingSuspensions.orgId, orgId)), + ) + .pipe(Effect.mapError(toDatabaseError)) + +// --- Core logic (exported for tests) ---------------------------------------- + +// Webhook core: reconcile one org's overdue row from its (already-fetched) +// Autumn customer. past_due → ensure an overdue row; otherwise clear it. +export const applyOverdueState = ( + database: DatabaseShape, + orgId: string, + customer: BillingCustomer, + nowMs: number, +): Effect.Effect => + isPastDue(customer) + ? upsertOverdue(database, orgId, nowMs).pipe(Effect.asVoid) + : clearOrg(database, orgId).pipe(Effect.asVoid) + +// Cron core: scope is the overdue set only (rows already present), never a +// full-org scan. Promotes pending rows past the grace window and clears settled +// ones. Per-org failures are logged and skipped so one bad org can't abort the sweep. +export const reconcileSuspensions = ( + database: DatabaseShape, + fetchCustomer: FetchCustomer, + nowMs: number, +): Effect.Effect => + Effect.gen(function* () { + const rows = yield* database + .execute((db) => db.select().from(orgBillingSuspensions)) + .pipe(Effect.mapError(toDatabaseError)) + + let suspended = 0 + let cleared = 0 + + yield* Effect.forEach( + rows, + (row) => + Effect.gen(function* () { + const alreadySuspended = row.suspendedAt !== null + // Invoices are only needed to evaluate a pending promotion. + const customer = yield* fetchCustomer(row.orgId, !alreadySuspended) + + // Paid / reactivated → no longer past_due → clear (ingestion resumes). + if (!isPastDue(customer)) { + yield* clearOrg(database, row.orgId) + cleared += 1 + return + } + // Already suspended and still past_due → stay suspended. + if (alreadySuspended) return + + const decision = shouldSuspend({ + customer, + overdueSince: row.overdueSince.getTime(), + now: nowMs, + }) + if (decision.suspend) { + yield* markSuspended(database, row.orgId, nowMs, decision.overdueInvoiceId) + suspended += 1 + } + }).pipe( + Effect.catch((error) => + Effect.logError("[billing] reconcile failed for org").pipe( + Effect.annotateLogs({ + orgId: row.orgId, + error: error instanceof Error ? error.message : String(error), + }), + ), + ), + ), + { discard: true }, + ) + + yield* Effect.annotateCurrentSpan({ + "billing.reconcile.scanned": rows.length, + "billing.reconcile.suspended": suspended, + "billing.reconcile.cleared": cleared, + }) + return { scanned: rows.length, suspended, cleared } satisfies ReconcileResult + }) + +// Owns the `org_billing_suspensions` table. The Autumn webhook drives +// `refreshOverdueState` (maintains the overdue clock authoritatively per org); +// the daily cron drives `runReconcile` (promotes overdue ≥3d + never-paid to +// suspended, and clears rows whose org has paid). The ingest gateway reads the +// resulting `suspended_at` flag directly from Postgres — never from this service. +export class BillingSuspensionService extends Context.Service()( + "@maple/api/services/BillingSuspensionService", + { + make: Effect.gen(function* () { + const database = yield* Database + const env = yield* Env + const secretKey = Option.match(env.AUTUMN_SECRET_KEY, { + onNone: () => undefined, + onSome: (value) => Redacted.value(value), + }) + const callAutumn = makeCallAutumn(secretKey) + + const fetchCustomer: FetchCustomer = (orgId, expandInvoices) => + callAutumn( + "getOrCreateCustomer", + expandInvoices ? { expand: ["invoices"] } : {}, + orgId, + ).pipe( + Effect.flatMap(ensureOk), + Effect.flatMap((response) => decodeUpstream(BillingCustomer, response)), + ) + + // Webhook entry point: re-derive an org's overdue state from Autumn (the + // authoritative source) and reconcile its row. No-op when billing is + // unconfigured so the webhook still 200s. + const refreshOverdueState = Effect.fn("BillingSuspensionService.refreshOverdueState")( + function* (orgId: string) { + if (secretKey === undefined) return + const now = yield* Clock.currentTimeMillis + const customer = yield* fetchCustomer(orgId, false) + yield* applyOverdueState(database, orgId, customer, now) + }, + ) + + // Cron entry point. + const runReconcile = Effect.fn("BillingSuspensionService.runReconcile")(function* () { + if (secretKey === undefined) { + yield* Effect.logWarning( + "[billing] reconcile skipped: AUTUMN_SECRET_KEY not configured", + ) + return { scanned: 0, suspended: 0, cleared: 0 } satisfies ReconcileResult + } + const now = yield* Clock.currentTimeMillis + return yield* reconcileSuspensions(database, fetchCustomer, now) + }) + + return { refreshOverdueState, runReconcile } + }), + }, +) { + static readonly layer = Layer.effect(this, this.make) + + static readonly refreshOverdueState = (orgId: string) => + this.use((service) => service.refreshOverdueState(orgId)) + + static readonly runReconcile = () => this.use((service) => service.runReconcile()) +} diff --git a/apps/api/src/worker.ts b/apps/api/src/worker.ts index deb6ae10..331b5741 100644 --- a/apps/api/src/worker.ts +++ b/apps/api/src/worker.ts @@ -236,10 +236,30 @@ const handleQueue = async ( } } -// Cron handler (every 12h, see wrangler.jsonc `triggers.crons`): enqueue a -// periodic VCS sync per installation. Single cron expression — no `event.cron` -// dispatch needed. -const handleScheduled = async (env: Record, ctx: ExecutionContext): Promise => { +// Cron expressions (must match `crons` in alchemy.run.ts). Dispatched on +// `event.cron` since the worker now registers more than one schedule. +const BILLING_RECONCILE_CRON = "0 6 * * *" + +// Daily billing-suspension reconcile: promote overdue-≥3d-and-never-paid orgs to +// suspended, clear orgs that have paid. +const handleBillingReconcile = async ( + env: Record, + ctx: ExecutionContext, +): Promise => { + const { buildBillingSuspensionLayer, runBillingSuspensionReconcile, flushBillingTelemetry } = + await import("./billing-suspension-runtime") + try { + await runScheduledEffect(buildBillingSuspensionLayer(env), runBillingSuspensionReconcile, ctx) + } finally { + ctx.waitUntil(flushBillingTelemetry(env)) + } +} + +// Periodic VCS sync (every 12h): enqueue a refresh per installation. +const handleVcsScheduledSync = async ( + env: Record, + ctx: ExecutionContext, +): Promise => { const { buildVcsScheduledLayer, runScheduledSync, flushVcsTelemetry } = await import("./vcs-sync-runtime") try { await runScheduledEffect(buildVcsScheduledLayer(env), runScheduledSync, ctx) @@ -248,11 +268,25 @@ const handleScheduled = async (env: Record, ctx: ExecutionConte } } +// Cron handler — route by the firing schedule. The billing reconcile is matched +// explicitly; every other expression (i.e. the 12h VCS sync) falls through. +const handleScheduled = async ( + cron: string, + env: Record, + ctx: ExecutionContext, +): Promise => { + if (cron === BILLING_RECONCILE_CRON) { + await handleBillingReconcile(env, ctx) + return + } + await handleVcsScheduledSync(env, ctx) +} + export default { fetch: (request: Request, env: Record, ctx: ExecutionContext) => handle(request, env, ctx), queue: (batch: MessageBatch, env: Record, ctx: ExecutionContext) => handleQueue(batch, env, ctx), - scheduled: (_event: ScheduledController, env: Record, ctx: ExecutionContext) => - handleScheduled(env, ctx), + scheduled: (event: ScheduledController, env: Record, ctx: ExecutionContext) => + handleScheduled(event.cron, env, ctx), } diff --git a/apps/api/wrangler.jsonc b/apps/api/wrangler.jsonc index af677dd5..78b329f7 100644 --- a/apps/api/wrangler.jsonc +++ b/apps/api/wrangler.jsonc @@ -7,9 +7,10 @@ "dev": { "port": 3472, }, - // Periodic VCS sync backstop every 12h — handler: worker.ts `scheduled`. (Mirrored in alchemy.run.ts for deploy.) + // Cron schedules — handler: worker.ts `scheduled` (dispatched by event.cron). (Mirrored in alchemy.run.ts for deploy.) + // - "0 *\/12 * * *": VCS sync backstop. "0 6 * * *": daily billing-suspension reconcile. "triggers": { - "crons": ["0 */12 * * *"], + "crons": ["0 */12 * * *", "0 6 * * *"], }, // Local dev: wrangler connects the binding straight to localConnectionString // (the docker-compose Postgres; see `bun run db:up` + `bun run db:migrate:local`). diff --git a/apps/ingest/src/main.rs b/apps/ingest/src/main.rs index 5d6b06db..52321857 100644 --- a/apps/ingest/src/main.rs +++ b/apps/ingest/src/main.rs @@ -552,6 +552,10 @@ struct KeyRow { org_id: String, self_managed: bool, clickhouse_ready: bool, + // The owning org's ingestion is suspended for non-payment (a row in + // org_billing_suspensions with suspended_at set). Resolved in the same + // roundtrip as routing; enforced as a 402 before any payload is accepted. + ingest_suspended: bool, } #[derive(Clone, Debug)] @@ -562,6 +566,7 @@ struct ConnectorRow { dataset: String, self_managed: bool, clickhouse_ready: bool, + ingest_suspended: bool, } #[derive(Clone, Debug)] @@ -594,6 +599,7 @@ impl IngestKeyIdentity { key_id: self.key_id, self_managed: routing.self_managed, clickhouse_ready: routing.clickhouse_ready, + ingest_suspended: routing.ingest_suspended, } } } @@ -619,6 +625,7 @@ impl CloudflareConnectorIdentity { secret_key_id: self.secret_key_id, self_managed: routing.self_managed, clickhouse_ready: routing.clickhouse_ready, + ingest_suspended: routing.ingest_suspended, } } } @@ -627,6 +634,10 @@ impl CloudflareConnectorIdentity { struct OrgRouting { self_managed: bool, clickhouse_ready: bool, + // Billing suspension flag (see KeyRow). Lives on routing so it refreshes on + // the short org-routing TTL — a paid-up org resumes ingestion quickly, and a + // routing fetch failure defaults to false (fail-open, never blocks on a DB blip). + ingest_suspended: bool, } impl OrgRouting { @@ -634,6 +645,7 @@ impl OrgRouting { Self { self_managed: row.self_managed, clickhouse_ready: row.clickhouse_ready, + ingest_suspended: row.ingest_suspended, } } @@ -641,6 +653,7 @@ impl OrgRouting { Self { self_managed: row.self_managed, clickhouse_ready: row.clickhouse_ready, + ingest_suspended: row.ingest_suspended, } } } @@ -685,6 +698,9 @@ struct ResolvedIngestKey { // version (SCHEMA_VERSION) — NOT the Tinybird-coupled PROJECT_REVISION, so a // Tinybird-only schema change can't silently un-ready a BYO-CH org. clickhouse_ready: bool, + // The org is 3+ days overdue and has never paid — ingestion is hard-stopped + // with a 402 before any payload is accepted. See org_billing_suspensions. + ingest_suspended: bool, } #[derive(Clone)] @@ -699,6 +715,7 @@ struct ResolvedCloudflareConnector { // to the self-managed pool when the owning org has BYO Tinybird active. self_managed: bool, clickhouse_ready: bool, + ingest_suspended: bool, } #[derive(Clone, Copy)] @@ -1554,6 +1571,7 @@ async fn resolve_grpc_ingest_key( key_id: "sentinel".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }); } @@ -2229,6 +2247,25 @@ async fn handle_signal_inner( "Authenticated" ); + // --- Billing suspension (dunning) --- + // Hard-stop ingestion for an org that is 3+ days overdue and has never paid. + // This flag is computed off-band by the API (Autumn webhook + daily reconcile) + // and persisted in org_billing_suspensions; the gateway only reads it. Checked + // before the Autumn entitlement gate and independent of AUTUMN_ENFORCE_LIMITS. + if resolved_key.ingest_suspended { + warn!( + org_id = %resolved_key.org_id, + "Ingestion suspended: unpaid invoice overdue" + ); + return Err(( + ApiError::new( + StatusCode::PAYMENT_REQUIRED, + "Ingestion suspended: unpaid invoice overdue", + ), + "billing_suspended", + )); + } + // --- Billing entitlement (per-signal) --- // Reject ingestion when the org has no active subscription or has exhausted // its hard-capped base-plan allotment for this signal. Fails open on any @@ -2404,6 +2441,23 @@ async fn handle_cloudflare_logpush_inner( Span::current().record("maple.ingest.self_managed", resolved.self_managed); Span::current().record("maple.ingest.clickhouse_ready", resolved.clickhouse_ready); + // Billing suspension (dunning) — same hard-stop as the OTLP path, before the + // per-feature entitlement gate and independent of AUTUMN_ENFORCE_LIMITS. + if resolved.ingest_suspended { + warn!( + org_id = %resolved.org_id, + connector_id, + "Cloudflare logpush suspended: unpaid invoice overdue" + ); + return Err(( + ApiError::new( + StatusCode::PAYMENT_REQUIRED, + "Ingestion suspended: unpaid invoice overdue", + ), + "billing_suspended", + )); + } + // Logpush bills the `logs` feature — gate it the same way as OTLP logs. if let Some(entitlements) = &state.autumn_entitlements { if !entitlements.is_allowed(&resolved.org_id, "logs").await { @@ -2527,6 +2581,7 @@ async fn handle_cloudflare_logpush_inner( key_id: resolved.secret_key_id.clone(), self_managed: resolved.self_managed, clickhouse_ready: resolved.clickhouse_ready, + ingest_suspended: resolved.ingest_suspended, }; let decoded = DecodedPayload::Logs(request); let response = match process_decoded_payload( @@ -3678,9 +3733,11 @@ impl KeyStore for PostgresKeyStore { let sql = format!( "SELECT k.org_id, \ COALESCE(s.sync_status = 'connected', false) AS self_managed, \ - COALESCE(s.sync_status = 'connected' AND s.schema_version = $1, false) AS clickhouse_ready \ + COALESCE(s.sync_status = 'connected' AND s.schema_version = $1, false) AS clickhouse_ready, \ + (b.suspended_at IS NOT NULL) AS ingest_suspended \ FROM org_ingest_keys k \ LEFT JOIN org_clickhouse_settings s ON s.org_id = k.org_id \ + LEFT JOIN org_billing_suspensions b ON b.org_id = k.org_id \ WHERE k.{hash_column} = $2 LIMIT 1" ); let client = self.client().await?; @@ -3695,6 +3752,7 @@ impl KeyStore for PostgresKeyStore { org_id: row.get("org_id"), self_managed: row.get("self_managed"), clickhouse_ready: row.get("clickhouse_ready"), + ingest_suspended: row.get("ingest_suspended"), })) } @@ -3709,9 +3767,11 @@ impl KeyStore for PostgresKeyStore { .query( "SELECT c.org_id, c.service_name, c.zone_name, c.dataset, \ COALESCE(s.sync_status = 'connected', false) AS self_managed, \ - COALESCE(s.sync_status = 'connected' AND s.schema_version = $1, false) AS clickhouse_ready \ + COALESCE(s.sync_status = 'connected' AND s.schema_version = $1, false) AS clickhouse_ready, \ + (b.suspended_at IS NOT NULL) AS ingest_suspended \ FROM cloudflare_logpush_connectors c \ LEFT JOIN org_clickhouse_settings s ON s.org_id = c.org_id \ + LEFT JOIN org_billing_suspensions b ON b.org_id = c.org_id \ WHERE c.id = $2 AND c.secret_hash = $3 AND c.enabled = true LIMIT 1", &[&revision, &connector_id, &secret_hash], ) @@ -3727,6 +3787,7 @@ impl KeyStore for PostgresKeyStore { dataset: row.get("dataset"), self_managed: row.get("self_managed"), clickhouse_ready: row.get("clickhouse_ready"), + ingest_suspended: row.get("ingest_suspended"), })) } @@ -3813,9 +3874,12 @@ impl KeyStore for PostgresKeyStore { let client = self.client().await?; let rows = client .query( - "SELECT COALESCE(sync_status = 'connected', false) AS self_managed, \ - COALESCE(sync_status = 'connected' AND schema_version = $1, false) AS clickhouse_ready \ - FROM org_clickhouse_settings WHERE org_id = $2 LIMIT 1", + "SELECT COALESCE(s.sync_status = 'connected', false) AS self_managed, \ + COALESCE(s.sync_status = 'connected' AND s.schema_version = $1, false) AS clickhouse_ready, \ + (b.suspended_at IS NOT NULL) AS ingest_suspended \ + FROM (SELECT $2::text AS org_id) o \ + LEFT JOIN org_clickhouse_settings s ON s.org_id = o.org_id \ + LEFT JOIN org_billing_suspensions b ON b.org_id = o.org_id LIMIT 1", &[&revision, &org_id], ) .await @@ -3826,6 +3890,7 @@ impl KeyStore for PostgresKeyStore { Ok(Some(OrgRouting { self_managed: row.get("self_managed"), clickhouse_ready: row.get("clickhouse_ready"), + ingest_suspended: row.get("ingest_suspended"), })) } @@ -3887,6 +3952,7 @@ impl KeyStore for StaticKeyStore { org_id: self.org_id.clone(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, })) } @@ -4232,6 +4298,7 @@ mod tests { key_id: "abc".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }; enrich_resource_attributes(&mut attributes, &resolved); @@ -4319,6 +4386,7 @@ mod tests { secret_key_id: "secret".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }; let record = serde_json::from_str::>( r#"{ @@ -4439,6 +4507,7 @@ mod tests { OrgRouting { self_managed: row.self_managed, clickhouse_ready: row.clickhouse_ready, + ingest_suspended: row.ingest_suspended, }, ); self.keys @@ -4454,6 +4523,7 @@ mod tests { OrgRouting { self_managed: row.self_managed, clickhouse_ready: row.clickhouse_ready, + ingest_suspended: row.ingest_suspended, }, ); self.connectors @@ -4823,6 +4893,7 @@ mod tests { org_id: "org_shared".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }, ); @@ -4846,6 +4917,7 @@ mod tests { org_id: "org_byo".to_string(), self_managed: true, clickhouse_ready: true, + ingest_suspended: false, }, ); @@ -4860,6 +4932,53 @@ mod tests { assert!(resolved.clickhouse_ready); } + #[tokio::test] + async fn resolve_ingest_key_propagates_ingest_suspended_flag() { + let store = Arc::new(FakeKeyStore::default()); + store.insert_private( + "maple_sk_test_suspended", + KeyRow { + org_id: "org_overdue".to_string(), + self_managed: false, + clickhouse_ready: false, + ingest_suspended: true, + }, + ); + + let resolved = make_resolver(store) + .resolve_ingest_key("maple_sk_test_suspended") + .await + .expect("resolve should succeed") + .expect("key should be found"); + + // A suspended org still resolves (auth succeeds) — the 402 is enforced by + // handle_signal_inner off this flag, not by dropping the key. + assert_eq!(resolved.org_id, "org_overdue"); + assert!(resolved.ingest_suspended); + } + + #[tokio::test] + async fn resolve_ingest_key_is_not_suspended_by_default() { + let store = Arc::new(FakeKeyStore::default()); + store.insert_private( + "maple_sk_test_paid", + KeyRow { + org_id: "org_paid".to_string(), + self_managed: false, + clickhouse_ready: false, + ingest_suspended: false, + }, + ); + + let resolved = make_resolver(store) + .resolve_ingest_key("maple_sk_test_paid") + .await + .expect("resolve should succeed") + .expect("key should be found"); + + assert!(!resolved.ingest_suspended); + } + #[tokio::test] async fn resolve_ingest_key_keeps_stale_schema_on_managed_native_path() { let store = Arc::new(FakeKeyStore::default()); @@ -4869,6 +4988,7 @@ mod tests { org_id: "org_stale".to_string(), self_managed: true, clickhouse_ready: false, + ingest_suspended: false, }, ); @@ -4895,6 +5015,7 @@ mod tests { org_id: "org_transition".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }, ); @@ -4912,6 +5033,7 @@ mod tests { OrgRouting { self_managed: true, clickhouse_ready: true, + ingest_suspended: false, }, ); tokio::time::sleep(Duration::from_millis(10)).await; @@ -4944,6 +5066,7 @@ mod tests { org_id: "org_d1_blip".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }, ); @@ -4960,6 +5083,7 @@ mod tests { OrgRouting { self_managed: true, clickhouse_ready: true, + ingest_suspended: false, }, ); tokio::time::sleep(Duration::from_millis(10)).await; @@ -5007,6 +5131,7 @@ mod tests { dataset: "http_requests".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }, ); let routing = make_routing_resolver(Arc::clone(&store), Duration::from_millis(5)); @@ -5034,6 +5159,7 @@ mod tests { OrgRouting { self_managed: true, clickhouse_ready: true, + ingest_suspended: false, }, ); tokio::time::sleep(Duration::from_millis(10)).await; @@ -5088,6 +5214,7 @@ mod tests { org_id: "org_forward_ready".to_string(), self_managed: false, clickhouse_ready: false, + ingest_suspended: false, }, ); let state = test_app_state( @@ -5128,6 +5255,7 @@ mod tests { OrgRouting { self_managed: true, clickhouse_ready: true, + ingest_suspended: false, }, ); store.insert_clickhouse_target( diff --git a/packages/db/drizzle/0003_lowly_fantastic_four.sql b/packages/db/drizzle/0003_lowly_fantastic_four.sql new file mode 100644 index 00000000..e8ac6051 --- /dev/null +++ b/packages/db/drizzle/0003_lowly_fantastic_four.sql @@ -0,0 +1,10 @@ +CREATE TABLE "org_billing_suspensions" ( + "org_id" text NOT NULL, + "overdue_since" timestamp with time zone NOT NULL, + "suspended_at" timestamp with time zone, + "overdue_invoice_id" text, + "reason" text, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + CONSTRAINT "org_billing_suspensions_org_id_pk" PRIMARY KEY("org_id") +); diff --git a/packages/db/drizzle/meta/0003_snapshot.json b/packages/db/drizzle/meta/0003_snapshot.json new file mode 100644 index 00000000..465cfe58 --- /dev/null +++ b/packages/db/drizzle/meta/0003_snapshot.json @@ -0,0 +1,5677 @@ +{ + "id": "fcf4be03-8be5-4562-81b6-cb3796c8dbb0", + "prevId": "bbf78dc1-583d-473f-9a6f-62296755db5d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_triage_runs": { + "name": "ai_triage_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_kind": { + "name": "incident_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_id": { + "name": "incident_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ai_triage_runs_incident_idx": { + "name": "ai_triage_runs_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_triage_runs_org_issue_idx": { + "name": "ai_triage_runs_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_triage_runs_org_created_idx": { + "name": "ai_triage_runs_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_triage_settings": { + "name": "ai_triage_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "max_runs_per_day": { + "name": "max_runs_per_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_delivery_events": { + "name": "alert_delivery_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_id": { + "name": "incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination_id": { + "name": "destination_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_message": { + "name": "provider_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_reference": { + "name": "provider_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_code": { + "name": "response_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_delivery_events_org_idx": { + "name": "alert_delivery_events_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_org_incident_idx": { + "name": "alert_delivery_events_org_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_due_idx": { + "name": "alert_delivery_events_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_claim_idx": { + "name": "alert_delivery_events_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claim_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_delivery_attempt_idx": { + "name": "alert_delivery_events_delivery_attempt_idx", + "columns": [ + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_destinations": { + "name": "alert_destinations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_iv": { + "name": "secret_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_tag": { + "name": "secret_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_destinations_org_idx": { + "name": "alert_destinations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_destinations_org_enabled_idx": { + "name": "alert_destinations_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_destinations_org_name_idx": { + "name": "alert_destinations_org_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_incidents": { + "name": "alert_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_key": { + "name": "incident_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_name": { + "name": "rule_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparator": { + "name": "comparator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "threshold_upper": { + "name": "threshold_upper", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "first_triggered_at": { + "name": "first_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_observed_value": { + "name": "last_observed_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_delivered_event_type": { + "name": "last_delivered_event_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_notified_at": { + "name": "last_notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_issue_id": { + "name": "error_issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_incidents_org_idx": { + "name": "alert_incidents_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_org_status_idx": { + "name": "alert_incidents_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_org_rule_idx": { + "name": "alert_incidents_org_rule_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_org_issue_idx": { + "name": "alert_incidents_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "error_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_incident_key_idx": { + "name": "alert_incidents_incident_key_idx", + "columns": [ + { + "expression": "incident_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_states": { + "name": "alert_rule_states", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'__total__'" + }, + "consecutive_breaches": { + "name": "consecutive_breaches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consecutive_healthy": { + "name": "consecutive_healthy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_rule_states_org_idx": { + "name": "alert_rule_states_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "alert_rule_states_org_id_rule_id_group_key_pk": { + "name": "alert_rule_states_org_id_rule_id_group_key_pk", + "columns": [ + "org_id", + "rule_id", + "group_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notification_template_json": { + "name": "notification_template_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_names_json": { + "name": "service_names_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exclude_service_names_json": { + "name": "exclude_service_names_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags_json": { + "name": "tags_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparator": { + "name": "comparator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "threshold_upper": { + "name": "threshold_upper", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "window_minutes": { + "name": "window_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minimum_sample_count": { + "name": "minimum_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consecutive_breaches_required": { + "name": "consecutive_breaches_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "consecutive_healthy_required": { + "name": "consecutive_healthy_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "renotify_interval_minutes": { + "name": "renotify_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "metric_name": { + "name": "metric_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metric_type": { + "name": "metric_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metric_aggregation": { + "name": "metric_aggregation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apdex_threshold_ms": { + "name": "apdex_threshold_ms", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "query_builder_draft_json": { + "name": "query_builder_draft_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_query_sql": { + "name": "raw_query_sql", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_by": { + "name": "group_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destination_ids_json": { + "name": "destination_ids_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "query_spec_json": { + "name": "query_spec_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reducer": { + "name": "reducer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sample_count_strategy": { + "name": "sample_count_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "no_data_behavior": { + "name": "no_data_behavior", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_scheduled_at": { + "name": "last_scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_rules_org_idx": { + "name": "alert_rules_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_rules_org_enabled_idx": { + "name": "alert_rules_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_rules_org_name_idx": { + "name": "alert_rules_org_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anomaly_detector_settings": { + "name": "anomaly_detector_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sensitivity": { + "name": "sensitivity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "muted_signals_json": { + "name": "muted_signals_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anomaly_detector_states": { + "name": "anomaly_detector_states", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_env": { + "name": "deployment_env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "fingerprint_hash": { + "name": "fingerprint_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_breaches": { + "name": "consecutive_breaches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consecutive_healthy": { + "name": "consecutive_healthy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "baseline_median": { + "name": "baseline_median", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "open_incident_id": { + "name": "open_incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_incident_id": { + "name": "last_incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "anomaly_detector_states_org_idx": { + "name": "anomaly_detector_states_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_detector_states_open_incident_idx": { + "name": "anomaly_detector_states_open_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "open_incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_detector_states_evaluated_idx": { + "name": "anomaly_detector_states_evaluated_idx", + "columns": [ + { + "expression": "last_evaluated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "anomaly_detector_states_org_id_detector_key_pk": { + "name": "anomaly_detector_states_org_id_detector_key_pk", + "columns": [ + "org_id", + "detector_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anomaly_incidents": { + "name": "anomaly_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_env": { + "name": "deployment_env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "fingerprint_hash": { + "name": "fingerprint_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_issue_id": { + "name": "error_issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_value": { + "name": "opened_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "baseline_median": { + "name": "baseline_median", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "baseline_sigma": { + "name": "baseline_sigma", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "last_observed_value": { + "name": "last_observed_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_triggered_at": { + "name": "first_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolve_reason": { + "name": "resolve_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "triage_status": { + "name": "triage_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprints_json": { + "name": "fingerprints_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reopen_count": { + "name": "reopen_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_reopened_at": { + "name": "last_reopened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "anomaly_incidents_org_status_idx": { + "name": "anomaly_incidents_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_incidents_org_triggered_idx": { + "name": "anomaly_incidents_org_triggered_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_triggered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_incidents_org_detector_idx": { + "name": "anomaly_incidents_org_detector_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "detector_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_incidents_org_issue_idx": { + "name": "anomaly_incidents_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "error_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_email": { + "name": "created_by_email", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_keys_org_id_idx": { + "name": "api_keys_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloudflare_logpush_connectors": { + "name": "cloudflare_logpush_connectors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_name": { + "name": "zone_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dataset": { + "name": "dataset", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'http_requests'" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_iv": { + "name": "secret_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_tag": { + "name": "secret_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_received_at": { + "name": "last_received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_rotated_at": { + "name": "secret_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cloudflare_logpush_connectors_org_idx": { + "name": "cloudflare_logpush_connectors_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cloudflare_logpush_connectors_org_enabled_idx": { + "name": "cloudflare_logpush_connectors_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cloudflare_logpush_connectors_secret_hash_unique": { + "name": "cloudflare_logpush_connectors_secret_hash_unique", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_versions": { + "name": "dashboard_versions", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "snapshot_json": { + "name": "snapshot_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "change_kind": { + "name": "change_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_version_id": { + "name": "source_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dashboard_versions_org_dashboard_idx": { + "name": "dashboard_versions_org_dashboard_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dashboard_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dashboard_versions_org_dashboard_version_unq": { + "name": "dashboard_versions_org_dashboard_version_unq", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dashboard_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dashboard_versions_org_id_id_pk": { + "name": "dashboard_versions_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "dashboards_org_updated_idx": { + "name": "dashboards_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dashboards_org_name_idx": { + "name": "dashboards_org_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dashboards_org_id_id_pk": { + "name": "dashboards_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.digest_subscriptions": { + "name": "digest_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "day_of_week": { + "name": "day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "last_sent_at": { + "name": "last_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "digest_subscriptions_org_user_idx": { + "name": "digest_subscriptions_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "digest_subscriptions_org_enabled_idx": { + "name": "digest_subscriptions_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.actors": { + "name": "actors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities_json": { + "name": "capabilities_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "actors_org_user_idx": { + "name": "actors_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_agent_name_idx": { + "name": "actors_org_agent_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_type_idx": { + "name": "actors_org_type_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_incidents": { + "name": "error_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_triggered_at": { + "name": "first_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "occurrence_count": { + "name": "occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_incidents_org_issue_idx": { + "name": "error_incidents_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_incidents_org_status_idx": { + "name": "error_incidents_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_issue_events": { + "name": "error_issue_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_state": { + "name": "from_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_state": { + "name": "to_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_issue_events_issue_idx": { + "name": "error_issue_events_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issue_events_actor_idx": { + "name": "error_issue_events_actor_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issue_events_type_idx": { + "name": "error_issue_events_type_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_issue_states": { + "name": "error_issue_states", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_observed_occurrence_at": { + "name": "last_observed_occurrence_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "open_incident_id": { + "name": "open_incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_issue_states_org_idx": { + "name": "error_issue_states_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "error_issue_states_org_id_issue_id_pk": { + "name": "error_issue_states_org_id_issue_id_pk", + "columns": [ + "org_id", + "issue_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_issues": { + "name": "error_issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'error'" + }, + "source_ref_json": { + "name": "source_ref_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fingerprint_hash": { + "name": "fingerprint_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exception_type": { + "name": "exception_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exception_message": { + "name": "exception_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_label": { + "name": "error_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "top_frame": { + "name": "top_frame", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_state": { + "name": "workflow_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'triage'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity_source": { + "name": "severity_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_holder_actor_id": { + "name": "lease_holder_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "occurrence_count": { + "name": "occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_actor_id": { + "name": "resolved_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snooze_until": { + "name": "snooze_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_issues_org_fp_idx": { + "name": "error_issues_org_fp_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_workflow_idx": { + "name": "error_issues_org_workflow_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_severity_idx": { + "name": "error_issues_org_severity_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_last_seen_idx": { + "name": "error_issues_org_last_seen_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_assignee_idx": { + "name": "error_issues_org_assignee_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assigned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_lease_expiry_idx": { + "name": "error_issues_lease_expiry_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_notification_policies": { + "name": "error_notification_policies", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "destination_ids_json": { + "name": "destination_ids_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "notify_on_first_seen": { + "name": "notify_on_first_seen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_on_regression": { + "name": "notify_on_regression", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_on_resolve": { + "name": "notify_on_resolve", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_on_transition_in_review": { + "name": "notify_on_transition_in_review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_on_transition_done": { + "name": "notify_on_transition_done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_on_claim": { + "name": "notify_on_claim", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "min_occurrence_count": { + "name": "min_occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_escalation_policies": { + "name": "issue_escalation_policies", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rules_json": { + "name": "rules_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_escalations": { + "name": "issue_escalations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "issue_escalations_dedupe_idx": { + "name": "issue_escalations_dedupe_idx", + "columns": [ + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_escalations_due_idx": { + "name": "issue_escalations_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_escalations_org_issue_idx": { + "name": "issue_escalations_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.investigations": { + "name": "investigations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'investigating'" + }, + "seeded_by": { + "name": "seeded_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "subject_json": { + "name": "subject_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "incident_kind": { + "name": "incident_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incident_id": { + "name": "incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_json": { + "name": "report_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "diagnosed_at": { + "name": "diagnosed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "investigations_incident_idx": { + "name": "investigations_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"investigations\".\"incident_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_created_idx": { + "name": "investigations_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_issue_idx": { + "name": "investigations_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_status_idx": { + "name": "investigations_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_auth_states": { + "name": "oauth_auth_states", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiated_by_user_id": { + "name": "initiated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_auth_states_expires_idx": { + "name": "oauth_auth_states_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_connections": { + "name": "oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_user_email": { + "name": "external_user_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "access_token_ciphertext": { + "name": "access_token_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_iv": { + "name": "access_token_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_tag": { + "name": "access_token_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_ciphertext": { + "name": "refresh_token_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_iv": { + "name": "refresh_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_tag": { + "name": "refresh_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_connections_org_provider_idx": { + "name": "oauth_connections_org_provider_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_connections_org_idx": { + "name": "oauth_connections_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_onboarding_state": { + "name": "org_onboarding_state", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "demo_data_requested": { + "name": "demo_data_requested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "checklist_dismissed_at": { + "name": "checklist_dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "first_data_received_at": { + "name": "first_data_received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "welcome_email_sent_at": { + "name": "welcome_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connect_nudge_email_sent_at": { + "name": "connect_nudge_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stalled_email_sent_at": { + "name": "stalled_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activation_email_sent_at": { + "name": "activation_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_billing_suspensions": { + "name": "org_billing_suspensions", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overdue_since": { + "name": "overdue_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "overdue_invoice_id": { + "name": "overdue_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_billing_suspensions_org_id_pk": { + "name": "org_billing_suspensions_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ingest_attribute_mappings": { + "name": "org_ingest_attribute_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_ingest_attribute_mappings_org_idx": { + "name": "org_ingest_attribute_mappings_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_recommendation_issues": { + "name": "org_recommendation_issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_key": { + "name": "canonical_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "org_recommendation_issues_org_idx": { + "name": "org_recommendation_issues_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_recommendation_issues_org_key_idx": { + "name": "org_recommendation_issues_org_key_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommendation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ingest_keys": { + "name": "org_ingest_keys", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key_hash": { + "name": "public_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_ciphertext": { + "name": "private_key_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_iv": { + "name": "private_key_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_tag": { + "name": "private_key_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_hash": { + "name": "private_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_rotated_at": { + "name": "public_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "private_rotated_at": { + "name": "private_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "org_ingest_keys_public_key_unique": { + "name": "org_ingest_keys_public_key_unique", + "columns": [ + { + "expression": "public_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_ingest_keys_public_key_hash_unique": { + "name": "org_ingest_keys_public_key_hash_unique", + "columns": [ + { + "expression": "public_key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_ingest_keys_private_key_hash_unique": { + "name": "org_ingest_keys_private_key_hash_unique", + "columns": [ + { + "expression": "private_key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_ingest_keys_org_id_pk": { + "name": "org_ingest_keys_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ingest_sampling_policies": { + "name": "org_ingest_sampling_policies", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trace_sample_ratio": { + "name": "trace_sample_ratio", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "always_keep_error_spans": { + "name": "always_keep_error_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "always_keep_slow_spans_ms": { + "name": "always_keep_slow_spans_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_clickhouse_settings": { + "name": "org_clickhouse_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ch_url": { + "name": "ch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ch_user": { + "name": "ch_user", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ch_password_ciphertext": { + "name": "ch_password_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ch_password_iv": { + "name": "ch_password_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ch_password_tag": { + "name": "ch_password_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ch_database": { + "name": "ch_database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_clickhouse_settings_org_id_pk": { + "name": "org_clickhouse_settings_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_clickhouse_schema_apply_runs": { + "name": "org_clickhouse_schema_apply_runs", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_migration": { + "name": "current_migration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steps_total": { + "name": "steps_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steps_done": { + "name": "steps_done", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applied_versions": { + "name": "applied_versions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_clickhouse_schema_apply_runs_org_id_pk": { + "name": "org_clickhouse_schema_apply_runs_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrape_target_checks": { + "name": "scrape_target_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "scrape_target_checks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_target_key": { + "name": "sub_target_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "samples_scraped": { + "name": "samples_scraped", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "samples_post_relabel": { + "name": "samples_post_relabel", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scrape_target_checks_target_checked_idx": { + "name": "scrape_target_checks_target_checked_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scrape_target_checks_target_id_scrape_targets_id_fk": { + "name": "scrape_target_checks_target_id_scrape_targets_id_fk", + "tableFrom": "scrape_target_checks", + "tableTo": "scrape_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrape_targets": { + "name": "scrape_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'prometheus'" + }, + "discovery_config_json": { + "name": "discovery_config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scrape_interval_seconds": { + "name": "scrape_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "labels_json": { + "name": "labels_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "auth_credentials_ciphertext": { + "name": "auth_credentials_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_credentials_iv": { + "name": "auth_credentials_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_credentials_tag": { + "name": "auth_credentials_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_scrape_at": { + "name": "last_scrape_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_scrape_error": { + "name": "last_scrape_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scrape_targets_org_idx": { + "name": "scrape_targets_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scrape_targets_org_enabled_idx": { + "name": "scrape_targets_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_commits": { + "name": "vcs_commits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha": { + "name": "sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_email": { + "name": "author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authored_at": { + "name": "authored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_commits_repo_sha_idx": { + "name": "vcs_commits_repo_sha_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_commits_org_sha_idx": { + "name": "vcs_commits_org_sha_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_installations": { + "name": "vcs_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_installation_id": { + "name": "external_installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_account_id": { + "name": "external_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_avatar_url": { + "name": "account_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_selection": { + "name": "repository_selection", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "installed_by_user_id": { + "name": "installed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_installations_provider_external_idx": { + "name": "vcs_installations_provider_external_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_installations_org_idx": { + "name": "vcs_installations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_repositories": { + "name": "vcs_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_repo_id": { + "name": "external_repo_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "tracked_branch": { + "name": "tracked_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_repositories_org_repo_idx": { + "name": "vcs_repositories_org_repo_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_repositories_org_idx": { + "name": "vcs_repositories_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_repositories_installation_idx": { + "name": "vcs_repositories_installation_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_repository_branches": { + "name": "vcs_repository_branches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_repository_branches_repo_name_idx": { + "name": "vcs_repository_branches_repo_name_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_repository_branches_org_idx": { + "name": "vcs_repository_branches_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 9352389b..83a3734a 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1782656894999, "tag": "0002_bizarre_triathlon", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1782846153383, + "tag": "0003_lowly_fantastic_four", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 382a0941..9c76710e 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -10,6 +10,7 @@ export * from "./escalations" export * from "./investigations" export * from "./oauth-connections" export * from "./onboarding" +export * from "./org-billing-suspensions" export * from "./org-ingest-attribute-mappings" export * from "./org-recommendation-issues" export * from "./org-ingest-keys" diff --git a/packages/db/src/schema/org-billing-suspensions.ts b/packages/db/src/schema/org-billing-suspensions.ts new file mode 100644 index 00000000..29b06fdf --- /dev/null +++ b/packages/db/src/schema/org-billing-suspensions.ts @@ -0,0 +1,29 @@ +import { pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core" + +// Per-org billing dunning state for the ingest gateway's "stop ingestion once +// 3 days overdue and never paid" policy. Written exclusively by the Autumn +// webhook receiver (`overdue_since`, on the `billing.updated` past_due signal) +// and the daily reconcile cron (`suspended_at`, once overdue ≥3d and the org +// has never paid an invoice). The ingest gateway reads this table during key +// resolution and 402s when `suspended_at IS NOT NULL`. A row with a null +// `suspended_at` is an org that is overdue but not yet suspended. +export const orgBillingSuspensions = pgTable( + "org_billing_suspensions", + { + orgId: text("org_id").notNull(), + // When Autumn first reported the subscription as past_due — the overdue clock. + overdueSince: timestamp("overdue_since", { withTimezone: true, mode: "date" }).notNull(), + // Set once the cron promotes the org to suspended (overdue ≥3d + never paid). + // Null = overdue-only. This column is the gateway's enforcement flag. + suspendedAt: timestamp("suspended_at", { withTimezone: true, mode: "date" }), + // The unpaid Stripe invoice id captured at suspension time (audit only). + overdueInvoiceId: text("overdue_invoice_id"), + reason: text("reason"), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull(), + }, + (table) => [primaryKey({ columns: [table.orgId] })], +) + +export type OrgBillingSuspensionRow = typeof orgBillingSuspensions.$inferSelect +export type OrgBillingSuspensionInsert = typeof orgBillingSuspensions.$inferInsert diff --git a/packages/domain/src/http/billing.ts b/packages/domain/src/http/billing.ts index 115d7aff..16e53af0 100644 --- a/packages/domain/src/http/billing.ts +++ b/packages/domain/src/http/billing.ts @@ -42,11 +42,23 @@ export class BillingSubscription extends Schema.Class("Bill quantity: Schema.optionalKey(Schema.Number), }) {} +// Present only when the customer is fetched with `expand: ["invoices"]` (the +// reconcile cron does this to decide "never paid"). Models the consumed subset +// of Autumn's `GetCustomerInvoice`; `status === "paid"` is the paid-history signal. +export class BillingInvoice extends Schema.Class("BillingInvoice")({ + stripeId: Schema.optionalKey(Schema.NullOr(Schema.String)), + status: Schema.optionalKey(Schema.NullOr(Schema.String)), + total: Schema.optionalKey(Schema.NullOr(Schema.Number)), + createdAt: Schema.optionalKey(Schema.NullOr(Schema.Number)), + hostedInvoiceUrl: Schema.optionalKey(Schema.NullOr(Schema.String)), +}) {} + export class BillingCustomer extends Schema.Class("BillingCustomer")({ id: Schema.String, subscriptions: Schema.Array(BillingSubscription), balances: Schema.optionalKey(Schema.Record(Schema.String, BillingBalance)), flags: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)), + invoices: Schema.optionalKey(Schema.Array(BillingInvoice)), }) {} // ---- Plan catalog (listPlans) ----