From 500dbdf09f316d284390de26f7fec5f3771657f3 Mon Sep 17 00:00:00 2001
From: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com>
Date: Thu, 15 Oct 2026 08:28:27 +0200
Subject: [PATCH] feat: opt-in Starter trial through Stripe Checkout (#2904)
* feat: opt-in Starter trial through Stripe Checkout
No trial starts at signup anymore. The owner of a free workspace who signed
in with GitHub or Google starts a 14-day Starter trial from a Stripe
Checkout page (no card required), from the billing page, the plan table or
the upgrade dialog, with an optional "what do you want to test?" note.
- getCheckoutSession({ trial, surface, intent }) runs the eligibility
guards before creating a Stripe customer
- sessionCompleted marks the trial: audit reason trial_started, customer
metadata trialed, user.trialed_at, trial_started event
- new user.trialed_at column; the trialEligibility query reads only the
database, the Checkout mutation adds the Stripe guards
- welcome email and marketing copy no longer promise an automatic trial
* feat: trial marker on the workspace, eligibility on workspace.get, upgrade intent dialog
- move trialed_at from user to workspace, next to trial_ends_at; the
webhook sets it through markWorkspaceTrialed
- workspace.get returns `trial` (eligibility without Stripe), checked
against the workspace row first; drop stripeRouter.trialEligibility
and its prefetch
- after ?success or ?trial on billing, ask "What are you building?" once
per user, stored as upgrade-intent feedback; feedback.hasSubmitted
* style(dashboard): taller textarea and ghost skip in the upgrade intent dialog
* feat: workspace.trialed_at migration, drop intent field from billing trial card
The post-checkout "What are you building?" dialog asks instead.
* fix: trial detection from checkout metadata, dedupe trial_started, upgrade_dialog surface
- classify opt-in trials by subscription.metadata.source, not live status
- track trial_started only when the workspace marker is first set
- show the plan-table trial caption only when Starter is listed
- index account.user_id for the provider lookup
- no credit card needed hint in the billing card and nav banner
* style(dashboard): ask what openstatus is used for in the upgrade intent dialog
* ci: apply automated fixes
* fix: share trial config, toast only once the trial is live
- TRIAL_DAYS and TrialSurface move to an import-free trial-config module
- the billing page polls workspace.get before the trial-started toast
- tailor wording in the upgrade intent dialog
- oxfmt
* ci: apply automated fixes
* fix(db): regenerate 0093 with account_user_id_idx
* chore(db): drop trialedAt comment
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
---
.../(dashboard)/settings/billing/client.tsx | 102 +-
.../settings/billing/search-params.ts | 1 +
.../data-table/billing/data-table.tsx | 47 +-
.../src/components/dialogs/upgrade-intent.tsx | 88 +
.../src/components/dialogs/upgrade.tsx | 17 +-
.../src/components/nav/nav-banner-upgrade.tsx | 14 +-
apps/dashboard/src/lib/auth/index.ts | 35 +-
apps/web/src/content/pages/home.mdx | 4 +-
.../pages/product/incident-management.mdx | 4 +-
.../src/content/pages/product/status-page.mdx | 4 +-
.../pages/product/uptime-monitoring.mdx | 4 +-
.../src/content/pages/unrelated/pricing.mdx | 6 +-
packages/api/src/router/feedback.test.ts | 44 +
packages/api/src/router/feedback.ts | 18 +
packages/api/src/router/stripe/index.ts | 68 +-
.../api/src/router/stripe/trial-config.ts | 4 +
.../src/router/stripe/trial-lifecycle.test.ts | 95 +
packages/api/src/router/stripe/trial.test.ts | 497 +-
packages/api/src/router/stripe/trial.ts | 223 +-
packages/api/src/router/stripe/webhook.ts | 42 +-
packages/api/src/router/workspace.ts | 10 +-
.../db/drizzle/0093_wonderful_red_skull.sql | 2 +
packages/db/drizzle/meta/0093_snapshot.json | 6907 +++++++++++++++++
packages/db/drizzle/meta/_journal.json | 7 +
packages/db/src/schema/feedbacks/constants.ts | 1 +
packages/db/src/schema/users/user.ts | 1 +
.../db/src/schema/workspaces/workspace.ts | 1 +
packages/emails/emails/welcome.tsx | 17 +-
packages/emails/src/templates.test.tsx | 14 +-
.../src/user/__tests__/providers.test.ts | 29 +
packages/services/src/user/index.ts | 7 +-
packages/services/src/user/providers.ts | 22 +
packages/services/src/user/schemas.ts | 3 +
.../src/workspace/__tests__/trial.test.ts | 136 +-
packages/services/src/workspace/index.ts | 3 +-
packages/services/src/workspace/trial.ts | 52 +-
36 files changed, 8075 insertions(+), 454 deletions(-)
create mode 100644 apps/dashboard/src/components/dialogs/upgrade-intent.tsx
create mode 100644 packages/api/src/router/feedback.test.ts
create mode 100644 packages/api/src/router/stripe/trial-config.ts
create mode 100644 packages/db/drizzle/0093_wonderful_red_skull.sql
create mode 100644 packages/db/drizzle/meta/0093_snapshot.json
create mode 100644 packages/services/src/user/__tests__/providers.test.ts
create mode 100644 packages/services/src/user/providers.ts
diff --git a/apps/dashboard/src/app/(dashboard)/settings/billing/client.tsx b/apps/dashboard/src/app/(dashboard)/settings/billing/client.tsx
index e93b194f2..ea00748da 100644
--- a/apps/dashboard/src/app/(dashboard)/settings/billing/client.tsx
+++ b/apps/dashboard/src/app/(dashboard)/settings/billing/client.tsx
@@ -1,12 +1,14 @@
"use client";
+import { TRIAL_DAYS } from "@openstatus/api/src/router/stripe/trial-config";
import { allPlans } from "@openstatus/db/src/schema/plan/config";
import type { Limits } from "@openstatus/db/src/schema/plan/schema";
import { Button } from "@openstatus/ui/components/ui/button";
+import { useCookieState } from "@openstatus/ui/hooks/use-cookie-state";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { useQueryStates } from "nuqs";
-import { useEffect, useMemo, useTransition } from "react";
+import { useEffect, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { BillingAddons } from "@/components/content/billing-addons";
@@ -23,6 +25,7 @@ import {
SectionTitle,
} from "@/components/content/section";
import { DataTable } from "@/components/data-table/billing/data-table";
+import { UpgradeIntentDialog } from "@/components/dialogs/upgrade-intent";
import {
FormCard,
FormCardContent,
@@ -76,9 +79,26 @@ export function Client() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const queryClient = useQueryClient();
- const [{ success, setup }, setSearchParams] =
+ const [{ success, setup, trial }, setSearchParams] =
useQueryStates(searchParamsParsers);
const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());
+ // Captured on mount: the toasts clear the params while the dialog is open.
+ const [askIntent, setAskIntent] = useState(Boolean(success || trial));
+ const { data: hasIntent } = useQuery({
+ ...trpc.feedback.hasSubmitted.queryOptions({ source: "upgrade-intent" }),
+ enabled: askIntent,
+ });
+ const [currency] = useCookieState("x-currency", "USD");
+ const trialCheckoutMutation = useMutation(
+ trpc.stripeRouter.getCheckoutSession.mutationOptions({
+ onSuccess: (data) => {
+ if (data?.type === "checkout" && data.session.url) {
+ window.location.assign(data.session.url);
+ }
+ },
+ onError: (error) => toast.error(error.message),
+ }),
+ );
const { data: usage } = useQuery(trpc.workspace.usage.queryOptions());
const paymentMethodSetupMutation = useMutation(
trpc.stripeRouter.getPaymentMethodSetupSession.mutationOptions({
@@ -122,6 +142,38 @@ export function Client() {
}
}, [success, setSearchParams]);
+ useEffect(() => {
+ if (!trial) return;
+ let cancelled = false;
+ queryClient.invalidateQueries({
+ queryKey: trpc.stripeRouter.getBillingInterval.queryKey(),
+ });
+ // The webhook can land after the redirect, and anyone can open
+ // `?trial=true`, so only toast once the workspace is actually trialing.
+ (async () => {
+ for (let attempt = 0; attempt < 5 && !cancelled; attempt++) {
+ const ws = await queryClient.fetchQuery({
+ ...trpc.workspace.get.queryOptions(),
+ staleTime: 0,
+ });
+ if (cancelled) return;
+ if (ws.trialDaysLeft !== null) {
+ toast.success(`Your ${TRIAL_DAYS}-day Starter trial has started`, {
+ duration: 5_000,
+ onAutoClose: () => setSearchParams({ trial: null }),
+ onDismiss: () => setSearchParams({ trial: null }),
+ });
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1_000));
+ }
+ if (!cancelled) setSearchParams({ trial: null });
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [trial, setSearchParams, queryClient, trpc]);
+
useEffect(() => {
if (setup) {
queryClient.invalidateQueries({
@@ -157,6 +209,10 @@ export function Client() {
return (
+
Billing
@@ -165,6 +221,48 @@ export function Client() {
+ {workspace?.trial.eligible ? (
+
+
+
+ Try Starter free for {TRIAL_DAYS} days
+
+
+ 1 min checks, more monitors, custom domain, Slack and on-call
+ integrations.
+
+
+
+
+
+ No credit card needed.
+ {" "}
+ The workspace returns to Hobby after {TRIAL_DAYS} days unless
+ you add a card.
+
+
+ trialCheckoutMutation.mutate({
+ trial: true,
+ plan: "starter",
+ interval: "monthly",
+ surface: "billing",
+ currency: currency || "USD",
+ workspaceSlug: workspace.slug,
+ successUrl: `${window.location.origin}/settings/billing?trial=true`,
+ cancelUrl: `${window.location.origin}/settings/billing`,
+ })
+ }
+ disabled={trialCheckoutMutation.isPending}
+ >
+ {trialCheckoutMutation.isPending
+ ? "Loading..."
+ : "Start trial"}
+
+
+
+ ) : null}
{workspace.trialEndsAt && trialDaysLeft ? (
diff --git a/apps/dashboard/src/app/(dashboard)/settings/billing/search-params.ts b/apps/dashboard/src/app/(dashboard)/settings/billing/search-params.ts
index 0e6b7e14c..84e66c386 100644
--- a/apps/dashboard/src/app/(dashboard)/settings/billing/search-params.ts
+++ b/apps/dashboard/src/app/(dashboard)/settings/billing/search-params.ts
@@ -3,6 +3,7 @@ import { createSearchParamsCache, parseAsBoolean } from "nuqs/server";
export const searchParamsParsers = {
success: parseAsBoolean,
setup: parseAsBoolean,
+ trial: parseAsBoolean,
};
export const searchParamsCache = createSearchParamsCache(searchParamsParsers);
diff --git a/apps/dashboard/src/components/data-table/billing/data-table.tsx b/apps/dashboard/src/components/data-table/billing/data-table.tsx
index 326b19966..26453ac01 100644
--- a/apps/dashboard/src/components/data-table/billing/data-table.tsx
+++ b/apps/dashboard/src/components/data-table/billing/data-table.tsx
@@ -1,5 +1,9 @@
"use client";
+import {
+ TRIAL_DAYS,
+ type TrialSurface,
+} from "@openstatus/api/src/router/stripe/trial-config";
import type { WorkspacePlan } from "@openstatus/db/src/schema";
import type {
Addons,
@@ -49,7 +53,13 @@ function getQuantitySuffix(addon: keyof Addons, interval: BillingInterval) {
return packSize > 1 ? `${period}/${packSize}` : `${period}/each`;
}
-export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {
+export function DataTable({
+ restrictTo,
+ surface = "plan_table",
+}: {
+ restrictTo?: WorkspacePlan[];
+ surface?: TrialSurface;
+}) {
const [interval, setInterval] = useState("monthly");
const [currency] = useCookieState("x-currency", "USD");
const trpc = useTRPC();
@@ -61,6 +71,7 @@ export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {
trpc.stripeRouter.getBillingInterval.queryOptions(),
);
const currentInterval = intervalQuery.data;
+ const canTrial = workspace?.trial.eligible === true;
const checkoutSessionMutation = useMutation(
trpc.stripeRouter.getCheckoutSession.mutationOptions({
@@ -128,7 +139,11 @@ export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {
{isTrialing
? "Upgrading ends your trial and charges your card today."
- : "A list to compare the different features by plan."}
+ : canTrial &&
+ interval === "monthly" &&
+ filteredPlans.some(({ id }) => id === "starter")
+ ? `Starter can be tried free for ${TRIAL_DAYS} days, no credit card needed.`
+ : "A list to compare the different features by plan."}
@@ -157,6 +172,8 @@ export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {
currentInterval !== null &&
currentInterval !== interval;
const price = getPriceConfig(id, currency, interval);
+ const isTrialOffer =
+ canTrial && id === "starter" && interval === "monthly";
return (
{plan.description}
+ {isTrialOffer ? (
+
+ Free for {TRIAL_DAYS} days. No credit card needed.
+
+ ) : null}
{isFreePlan ? (
@@ -196,6 +218,19 @@ export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {
onClick={() => {
startTransition(async () => {
try {
+ if (isTrialOffer) {
+ await checkoutSessionMutation.mutateAsync({
+ trial: true,
+ plan: "starter",
+ interval: "monthly",
+ surface,
+ currency: currency || "USD",
+ workspaceSlug: workspace.slug,
+ successUrl: `${BASE_URL}/settings/billing?trial=true`,
+ cancelUrl: `${BASE_URL}/settings/billing`,
+ });
+ return;
+ }
if (id === "free") {
await customerPortalMutation.mutateAsync({
workspaceSlug: workspace.slug,
@@ -232,9 +267,11 @@ export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) {
? "Choosing..."
: isIntervalSwitch
? `Switch to ${interval}`
- : isTrialing && !isFreePlan
- ? "Upgrade now"
- : "Choose"}
+ : isTrialOffer
+ ? `Start ${TRIAL_DAYS}-day trial`
+ : isTrialing && !isFreePlan
+ ? "Upgrade now"
+ : "Choose"}
diff --git a/apps/dashboard/src/components/dialogs/upgrade-intent.tsx b/apps/dashboard/src/components/dialogs/upgrade-intent.tsx
new file mode 100644
index 000000000..b2b424c7b
--- /dev/null
+++ b/apps/dashboard/src/components/dialogs/upgrade-intent.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { Button } from "@openstatus/ui/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@openstatus/ui/components/ui/dialog";
+import { Label } from "@openstatus/ui/components/ui/label";
+import { Textarea } from "@openstatus/ui/components/ui/textarea";
+import type { DialogProps } from "@radix-ui/react-dialog";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useId, useState } from "react";
+import { toast } from "sonner";
+
+import { useTRPC } from "@/lib/trpc/client";
+
+/** Asked after an upgrade or trial start; stored as `upgrade-intent` feedback. */
+export function UpgradeIntentDialog(props: DialogProps) {
+ const trpc = useTRPC();
+ const queryClient = useQueryClient();
+ const id = useId();
+ const [message, setMessage] = useState("");
+ const feedbackMutation = useMutation(
+ trpc.feedback.submit.mutationOptions({
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: trpc.feedback.hasSubmitted.queryKey({
+ source: "upgrade-intent",
+ }),
+ });
+ toast.success("Thanks for sharing");
+ props.onOpenChange?.(false);
+ },
+ onError: (error) => toast.error(error.message),
+ }),
+ );
+
+ return (
+
+
+
+ What are you using openstatus for?
+
+ A sentence or two helps us tailor openstatus to you.
+
+
+
+
+ What are you using openstatus for?
+
+
+
+ props.onOpenChange?.(false)}
+ disabled={feedbackMutation.isPending}
+ >
+ Skip
+
+
+ feedbackMutation.mutate({
+ source: "upgrade-intent",
+ message,
+ path: window.location.pathname,
+ })
+ }
+ disabled={feedbackMutation.isPending || !message.trim()}
+ >
+ {feedbackMutation.isPending ? "Sending..." : "Send"}
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/components/dialogs/upgrade.tsx b/apps/dashboard/src/components/dialogs/upgrade.tsx
index 3459661b7..34cf22c36 100644
--- a/apps/dashboard/src/components/dialogs/upgrade.tsx
+++ b/apps/dashboard/src/components/dialogs/upgrade.tsx
@@ -1,3 +1,4 @@
+import { TRIAL_DAYS } from "@openstatus/api/src/router/stripe/trial-config";
import type { WorkspacePlan } from "@openstatus/db/src/schema";
import { allPlans } from "@openstatus/db/src/schema/plan/config";
import type { Addons, Limits } from "@openstatus/db/src/schema/plan/schema";
@@ -47,6 +48,8 @@ export function UpgradeDialog(
};
const restrictTo = getRestrictTo();
+ const canTrial =
+ workspace?.trial.eligible === true && restrictTo.includes("starter");
const addon =
props.limit && Object.prototype.hasOwnProperty.call(planAddons, props.limit)
@@ -57,10 +60,16 @@ export function UpgradeDialog(
- Upgrade Workspace
+
+ {canTrial
+ ? `Try Starter free for ${TRIAL_DAYS} days`
+ : "Upgrade Workspace"}
+
- Upgrade your workspace to support more monitors, status pages,
- regions, and much more. Get an overview within your{" "}
+ {canTrial
+ ? "No credit card needed. Start the trial from the Starter column, or pick another plan. "
+ : "Upgrade your workspace to support more monitors, status pages, regions, and much more. "}
+ Get an overview within your{" "}
props.onOpenChange?.(false)}
href="/settings/billing"
@@ -97,7 +106,7 @@ export function UpgradeDialog(
) : (
-
+
)}
diff --git a/apps/dashboard/src/components/nav/nav-banner-upgrade.tsx b/apps/dashboard/src/components/nav/nav-banner-upgrade.tsx
index 5ce1df2c0..0deb6b196 100644
--- a/apps/dashboard/src/components/nav/nav-banner-upgrade.tsx
+++ b/apps/dashboard/src/components/nav/nav-banner-upgrade.tsx
@@ -1,5 +1,6 @@
"use client";
+import { TRIAL_DAYS } from "@openstatus/api/src/router/stripe/trial-config";
import { allPlans } from "@openstatus/db/src/schema/plan/config";
import { Clock, Close, Globe, Notification, Team } from "@openstatus/icons";
import { SlackIcon } from "@openstatus/icons/brand";
@@ -41,7 +42,11 @@ export function NavBannerUpgrade({ handleClose }: { handleClose: () => void }) {
return (
- Upgrade to {plan.title}
+
+ {workspace?.trial.eligible
+ ? `Try ${plan.title} free for ${TRIAL_DAYS} days`
+ : `Upgrade to ${plan.title}`}
+
void }) {
data-active="true"
onClick={() => setOpen(true)}
>
- Upgrade
+ {workspace?.trial.eligible ? "Start trial" : "Upgrade"}
+ {workspace?.trial.eligible ? (
+
+ No credit card needed
+
+ ) : null}
diff --git a/apps/dashboard/src/lib/auth/index.ts b/apps/dashboard/src/lib/auth/index.ts
index 0238d40b1..829b352c6 100644
--- a/apps/dashboard/src/lib/auth/index.ts
+++ b/apps/dashboard/src/lib/auth/index.ts
@@ -1,7 +1,6 @@
import { Events, setupAnalytics } from "@openstatus/analytics";
import { db, eq } from "@openstatus/db";
import { type User, user } from "@openstatus/db/src/schema";
-import { getCurrency } from "@openstatus/db/src/schema/plan/utils";
import { WelcomeEmail, sendEmail } from "@openstatus/emails";
import type { DefaultSession } from "next-auth";
import NextAuth from "next-auth";
@@ -47,10 +46,9 @@ async function syncUser(
}
// Runs from the `signIn` event, not `createUser`: only `signIn` carries the
-// account, and the trial must know the provider to skip SSO signups. It also
-// fires after the account row is linked, so nothing races the adapter.
-// Scheduled with `after()`: the Stripe calls take seconds and would otherwise
-// hold the sign-in redirect. Headers are read up front, before the response.
+// account, and it fires after the account row is linked, so nothing races the
+// adapter. Scheduled with `after()` so the mail does not hold the sign-in
+// redirect. Headers are read up front, before the response.
async function onNewUser(
newUser: Partial,
provider: string | undefined,
@@ -63,31 +61,11 @@ async function onNewUser(
// this means the user has already been created with clerk
if (newUser.tenantId) return;
- const currency = getCurrency({
- continent: requestHeaders.get("x-vercel-ip-continent") || "NA",
- country: requestHeaders.get("x-vercel-ip-country") || "US",
- });
-
- // Imported lazily to keep Stripe out of the proxy bundle, which also loads this module.
- const { maybeStartSignupTrial } =
- await import("@openstatus/api/src/router/stripe/trial");
- const trial = await maybeStartSignupTrial({
- userId: newUser.id,
- email: newUser.email,
- provider,
- currency,
- }).catch((error: Error) => {
- console.error("signup trial failed", { userId: newUser.id, error });
- return { started: false, reason: "stripe_error" } as const;
- });
-
await sendEmail({
from: "Thibault from openstatus ",
subject: "Welcome to openstatus.",
to: [newUser.email],
- react: WelcomeEmail({
- trialEndsAt: trial.started ? trial.trialEndsAt : undefined,
- }),
+ react: WelcomeEmail(),
});
const analytics = await setupAnalytics({
@@ -99,11 +77,6 @@ async function onNewUser(
});
await analytics.track({ ...Events.CreateUser, provider });
- if (trial.started) {
- await analytics.track({ ...Events.StartTrial, currency });
- } else if (trial.reason !== "disabled") {
- await analytics.track({ ...Events.SkipTrial, reason: trial.reason });
- }
}
const {
diff --git a/apps/web/src/content/pages/home.mdx b/apps/web/src/content/pages/home.mdx
index 18cd0e52c..876c6073f 100644
--- a/apps/web/src/content/pages/home.mdx
+++ b/apps/web/src/content/pages/home.mdx
@@ -25,7 +25,7 @@ faq:
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
@@ -258,7 +258,7 @@ Every action in the dashboard is reachable programmatically. One API key, four w
## Flat pricing
-No per-seat fees. Every new workspace starts with a 14-day Starter trial, no card needed. [Open source](/github) and self-hostable under AGPL-3.0.
+No per-seat fees. Try Starter free for 14 days from your billing settings, no card needed. [Open source](/github) and self-hostable under AGPL-3.0.
| Plan | Price | Includes |
| --- | --- | --- |
diff --git a/apps/web/src/content/pages/product/incident-management.mdx b/apps/web/src/content/pages/product/incident-management.mdx
index 73291bf90..1246642ea 100644
--- a/apps/web/src/content/pages/product/incident-management.mdx
+++ b/apps/web/src/content/pages/product/incident-management.mdx
@@ -30,7 +30,7 @@ faq:
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
@@ -227,7 +227,7 @@ Every one of those steps is in the audit log with the person behind it, whicheve
Declare your first incident
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
## Frequently asked questions
diff --git a/apps/web/src/content/pages/product/status-page.mdx b/apps/web/src/content/pages/product/status-page.mdx
index c8922c020..0bf39847f 100644
--- a/apps/web/src/content/pages/product/status-page.mdx
+++ b/apps/web/src/content/pages/product/status-page.mdx
@@ -38,7 +38,7 @@ faq:
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
@@ -324,7 +324,7 @@ The same page over SSH for people in a terminal, and as markdown for the agents
Create your status page
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
## Frequently asked questions
diff --git a/apps/web/src/content/pages/product/uptime-monitoring.mdx b/apps/web/src/content/pages/product/uptime-monitoring.mdx
index d6e1fc1c7..e1b8cd386 100644
--- a/apps/web/src/content/pages/product/uptime-monitoring.mdx
+++ b/apps/web/src/content/pages/product/uptime-monitoring.mdx
@@ -46,7 +46,7 @@ faq:
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
@@ -269,7 +269,7 @@ The response-logs table for the check, one row per region with status, latency a
Create your monitor
-Free to start, with a 14-day Starter trial and no card needed. Paid plans from $30/mo.
+Free to start. Try Starter free for 14 days, no card needed. Paid plans from $30/mo.
## Frequently asked questions
diff --git a/apps/web/src/content/pages/unrelated/pricing.mdx b/apps/web/src/content/pages/unrelated/pricing.mdx
index 7dc4b15dc..7d5abaab2 100644
--- a/apps/web/src/content/pages/unrelated/pricing.mdx
+++ b/apps/web/src/content/pages/unrelated/pricing.mdx
@@ -16,7 +16,7 @@ faq:
- question: "Do you offer a free plan?"
answer: "Yes. Our free Hobby plan includes one monitor, one status page with three components, and a minimum check interval of 10 minutes. No credit card required."
- question: "Is there a free trial?"
- answer: "Yes. New workspaces start with a 14-day trial of the Starter plan, no credit card required. If you have not added a card by then, the workspace returns to the free Hobby plan."
+ answer: "Yes. Sign in with GitHub or Google and start a 14-day Starter trial from Settings > Billing, no credit card required. If you have not added a card by then, the workspace returns to the free Hobby plan."
- question: "What are add-ons and how are they billed?"
answer: "Add-ons are workspace-level settings like White Label, Custom Theme, Email Authentication, IP Restriction, extra status pages, and extra monitors. Once enabled, they apply to your entire workspace. Add-ons are billed monthly regardless of your plan's billing cycle."
- question: "Can I upgrade or downgrade my plan?"
@@ -39,7 +39,7 @@ faq:
-New workspaces start with a 14-day Starter trial, no card needed.
+Try Starter free for 14 days, no card needed.
| Features comparison | $30/month - Starter | $100/month - Pro | $500/month - Scale |
| --- | --- | --- | --- |
@@ -98,7 +98,7 @@ Yes. Our free **Hobby** plan includes **one monitor**, **one status page with th
-Yes. New workspaces start with a **14-day trial of the Starter plan**, no credit card required. If you have not added a card by then, the workspace returns to the free Hobby plan.
+Yes. Sign in with GitHub or Google and start a **14-day Starter trial** from Settings > Billing, no credit card required. If you have not added a card by then, the workspace returns to the free Hobby plan.
diff --git a/packages/api/src/router/feedback.test.ts b/packages/api/src/router/feedback.test.ts
new file mode 100644
index 000000000..ec4390241
--- /dev/null
+++ b/packages/api/src/router/feedback.test.ts
@@ -0,0 +1,44 @@
+import { createTestWorkspace } from "@openstatus/db/src/test/factories";
+import { expect } from "@std/expect";
+import { test } from "@std/testing/bdd";
+
+import { edgeRouter } from "../edge";
+import { createInnerTRPCContext } from "../trpc";
+
+function caller(userId: number, workspaceId: number) {
+ return edgeRouter.createCaller(
+ createInnerTRPCContext({
+ req: undefined,
+ session: { user: { id: String(userId) } },
+ // @ts-expect-error - minimal user for test
+ user: { id: userId },
+ // @ts-expect-error - minimal workspace for test
+ workspace: { id: workspaceId },
+ }),
+ );
+}
+
+test("hasSubmitted turns true once the user answered that source", async () => {
+ const f = await createTestWorkspace();
+ const other = await createTestWorkspace();
+ const own = caller(f.user.id, f.workspace.id);
+ const source = "upgrade-intent" as const;
+
+ expect(await own.feedback.hasSubmitted({ source })).toBe(false);
+
+ await own.feedback.submit({
+ source,
+ message: "A status page for our API",
+ path: "/settings/billing",
+ });
+
+ expect(await own.feedback.hasSubmitted({ source })).toBe(true);
+ expect(await own.feedback.hasSubmitted({ source: "onboarding-source" })).toBe(
+ false,
+ );
+ expect(
+ await caller(other.user.id, other.workspace.id).feedback.hasSubmitted({
+ source,
+ }),
+ ).toBe(false);
+});
diff --git a/packages/api/src/router/feedback.ts b/packages/api/src/router/feedback.ts
index c19a51872..7b6d4677c 100644
--- a/packages/api/src/router/feedback.ts
+++ b/packages/api/src/router/feedback.ts
@@ -1,4 +1,5 @@
import { Events } from "@openstatus/analytics";
+import { and, eq } from "@openstatus/db";
import {
feedback,
feedbackSource,
@@ -10,6 +11,23 @@ import { env } from "../env";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const feedbackRouter = createTRPCRouter({
+ // Lets one-time prompts ask only until the user has answered once.
+ hasSubmitted: protectedProcedure
+ .input(z.object({ source: z.enum(feedbackSource) }))
+ .query(async (opts) => {
+ const row = await opts.ctx.db
+ .select({ id: feedback.id })
+ .from(feedback)
+ .where(
+ and(
+ eq(feedback.userId, opts.ctx.user.id),
+ eq(feedback.source, opts.input.source),
+ ),
+ )
+ .get();
+ return row !== undefined;
+ }),
+
submit: protectedProcedure
.meta({
track: Events.SubmitFeedback,
diff --git a/packages/api/src/router/stripe/index.ts b/packages/api/src/router/stripe/index.ts
index c09b23b19..4a8951f26 100644
--- a/packages/api/src/router/stripe/index.ts
+++ b/packages/api/src/router/stripe/index.ts
@@ -1,4 +1,4 @@
-import { Events } from "@openstatus/analytics";
+import { Events, setupAnalytics } from "@openstatus/analytics";
import { workspacePlans } from "@openstatus/db/src/schema";
import type {
AddonQuantityKey,
@@ -36,6 +36,12 @@ import {
syncedLimits,
trialEndsAtOf,
} from "./shared";
+import {
+ TRIAL_SKIP_MESSAGE,
+ type TrialSkipReason,
+ checkTrialEligibility,
+ trialCheckoutParams,
+} from "./trial";
import {
buildPlanChangeItems,
getPlanFromPriceId,
@@ -73,6 +79,23 @@ async function resolveWorkspaceCtx(opts: {
return { ctx, email: access.email };
}
+// The tracking middleware only fires on success, and a refused trial throws.
+async function trackSkipTrial(ctx: ServiceContext, reason: TrialSkipReason) {
+ const userId = "userId" in ctx.actor ? ctx.actor.userId : undefined;
+ try {
+ const analytics = await setupAnalytics({
+ userId: userId ? `usr_${userId}` : undefined,
+ workspaceId: String(ctx.workspace.id),
+ workspaceName: ctx.workspace.name || ctx.workspace.slug,
+ plan: ctx.workspace.plan,
+ source: "dashboard",
+ });
+ await analytics.track({ ...Events.SkipTrial, reason });
+ } catch (err) {
+ console.error("Failed to track trial_skipped:", err);
+ }
+}
+
async function ensureStripeCustomer(ctx: ServiceContext, email: string | null) {
if (ctx.workspace.stripeId) return ctx.workspace.stripeId;
@@ -121,6 +144,10 @@ const checkoutSessionInput = z.object({
interval: z.enum(billingIntervals).default("monthly"),
successUrl: z.string().optional(),
cancelUrl: z.string().optional(),
+ trial: z.boolean().default(false),
+ surface: z.enum(["billing", "plan_table", "upgrade_dialog"]).optional(),
+ // Stripe caps a metadata value at 500 characters.
+ intent: z.string().trim().max(500).optional(),
});
export const stripeRouter = createTRPCRouter({
@@ -173,8 +200,8 @@ export const stripeRouter = createTRPCRouter({
}
const parsed = checkoutSessionInput.safeParse(input);
if (!parsed.success) return {};
- const { plan, interval, currency } = parsed.data;
- return { plan, interval, currency };
+ const { plan, interval, currency, trial, surface } = parsed.data;
+ return { plan, interval, currency, trial, surface };
},
})
.input(checkoutSessionInput)
@@ -182,6 +209,32 @@ export const stripeRouter = createTRPCRouter({
const resolved = await resolveWorkspaceCtx(opts);
if (!resolved) return;
const result = resolved.ctx.workspace;
+
+ // Checked before the customer exists, so a refused trial leaves no
+ // Stripe customer behind.
+ if (opts.input.trial) {
+ if (
+ opts.input.plan !== "starter" ||
+ opts.input.interval !== "monthly"
+ ) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "The trial is only available on Starter monthly.",
+ });
+ }
+ const eligibility = await checkTrialEligibility({
+ ctx: resolved.ctx,
+ email: resolved.email,
+ });
+ if (!eligibility.eligible) {
+ await trackSkipTrial(resolved.ctx, eligibility.reason);
+ throw new TRPCError({
+ code: "PRECONDITION_FAILED",
+ message: TRIAL_SKIP_MESSAGE[eligibility.reason],
+ });
+ }
+ }
+
const stripeId = await ensureStripeCustomer(resolved.ctx, resolved.email);
const priceId = getPriceIdForPlan(opts.input.plan, opts.input.interval);
@@ -324,8 +377,15 @@ export const stripeRouter = createTRPCRouter({
enabled: true,
},
mode: "subscription",
- success_url: opts.input.successUrl || `${billingUrl}?success=true`,
+ success_url:
+ opts.input.successUrl ||
+ `${billingUrl}?${opts.input.trial ? "trial" : "success"}=true`,
cancel_url: opts.input.cancelUrl || billingUrl,
+ ...(opts.input.trial &&
+ trialCheckoutParams({
+ surface: opts.input.surface,
+ intent: opts.input.intent || undefined,
+ })),
});
return { type: "checkout" as const, session };
diff --git a/packages/api/src/router/stripe/trial-config.ts b/packages/api/src/router/stripe/trial-config.ts
new file mode 100644
index 000000000..10d8c35c9
--- /dev/null
+++ b/packages/api/src/router/stripe/trial-config.ts
@@ -0,0 +1,4 @@
+// Import-free so client bundles can share it.
+export const TRIAL_DAYS = 14;
+
+export type TrialSurface = "billing" | "plan_table" | "upgrade_dialog";
diff --git a/packages/api/src/router/stripe/trial-lifecycle.test.ts b/packages/api/src/router/stripe/trial-lifecycle.test.ts
index f5a9b82b5..f9801412c 100644
--- a/packages/api/src/router/stripe/trial-lifecycle.test.ts
+++ b/packages/api/src/router/stripe/trial-lifecycle.test.ts
@@ -115,6 +115,15 @@ function asCaller(fixture: Awaited>) {
});
}
+async function readTrialedAt(workspaceId: number) {
+ const row = await db
+ .select({ trialedAt: workspace.trialedAt })
+ .from(workspace)
+ .where(eq(workspace.id, workspaceId))
+ .get();
+ return row?.trialedAt;
+}
+
describe("trial lifecycle", () => {
let live: Stripe.Subscription[];
let customer: Stripe.Customer;
@@ -516,6 +525,92 @@ describe("trial lifecycle", () => {
assertSpyCalls(update, 2);
});
+ test("checkout completed with a trial → trial_started, customer stamped", async () => {
+ const stripeId = `cus_${crypto.randomUUID()}`;
+ const f = await createTestWorkspace({
+ plan: "free",
+ stripeId,
+ subscriptionId: null,
+ trialEndsAt: null,
+ });
+ const sub = subscription(stripeId, {
+ metadata: {
+ source: "trial_opt_in",
+ surface: "billing",
+ intent: "Slack alerts",
+ },
+ });
+ const retrieveSub = stub(stripe.subscriptions, "retrieve", () =>
+ Promise.resolve(sub as Stripe.Response),
+ );
+ const updateCustomer = stub(stripe.customers, "update", () =>
+ Promise.resolve({} as Stripe.Response),
+ );
+ stubs.push(retrieveSub, updateCustomer);
+ const session = {
+ mode: "subscription",
+ customer: stripeId,
+ subscription: sub.id,
+ } as Stripe.Checkout.Session;
+
+ await caller().sessionCompleted(
+ event("checkout.session.completed", session),
+ );
+
+ const after = await readWorkspace(f.workspace.id);
+ expect(after?.plan).toBe("starter");
+ expect(after?.subscriptionId).toBe(sub.id);
+ expect(after?.trialEndsAt).toEqual(new Date((sub.trial_end ?? 0) * 1000));
+ expect(await lastPlanAuditReason(f.workspace.id)).toBe("trial_started");
+ expect(updateCustomer.calls[0]?.args).toEqual([
+ stripeId,
+ { metadata: { trialed: "true" } },
+ ]);
+
+ const trialedAt = await readTrialedAt(f.workspace.id);
+ expect(trialedAt).toBeInstanceOf(Date);
+
+ // A redelivered event stamps again and changes nothing else.
+ await caller().sessionCompleted(
+ event("checkout.session.completed", session),
+ );
+ assertSpyCalls(updateCustomer, 2);
+ expect((await readWorkspace(f.workspace.id))?.plan).toBe("starter");
+ expect(await readTrialedAt(f.workspace.id)).toEqual(trialedAt);
+ });
+
+ test("checkout completed with a paid subscription is not a trial", async () => {
+ const stripeId = `cus_${crypto.randomUUID()}`;
+ const f = await createTestWorkspace({
+ plan: "free",
+ stripeId,
+ subscriptionId: null,
+ trialEndsAt: null,
+ });
+ const sub = subscription(stripeId, { status: "active", trial_end: null });
+ const retrieveSub = stub(stripe.subscriptions, "retrieve", () =>
+ Promise.resolve(sub as Stripe.Response),
+ );
+ const updateCustomer = stub(stripe.customers, "update", () =>
+ Promise.resolve({} as Stripe.Response),
+ );
+ stubs.push(retrieveSub, updateCustomer);
+
+ await caller().sessionCompleted(
+ event("checkout.session.completed", {
+ mode: "subscription",
+ customer: stripeId,
+ subscription: sub.id,
+ } as Stripe.Checkout.Session),
+ );
+
+ expect(await lastPlanAuditReason(f.workspace.id)).toBe(
+ "checkout_session_completed",
+ );
+ assertSpyCalls(updateCustomer, 0);
+ expect(await readTrialedAt(f.workspace.id)).toBeNull();
+ });
+
test("trialing → active clears trialEndsAt", async () => {
const s = await seedTrial();
const sub = subscription(s.stripeId, { status: "active" });
diff --git a/packages/api/src/router/stripe/trial.test.ts b/packages/api/src/router/stripe/trial.test.ts
index 2864d1296..c3df132ad 100644
--- a/packages/api/src/router/stripe/trial.test.ts
+++ b/packages/api/src/router/stripe/trial.test.ts
@@ -1,45 +1,96 @@
import { db, eq } from "@openstatus/db";
-import { invitation, workspace } from "@openstatus/db/src/schema";
-import { getLimits } from "@openstatus/db/src/schema/plan/utils";
-import { createTestWorkspace } from "@openstatus/db/src/test/factories";
+import {
+ account,
+ selectUserSchema,
+ selectWorkspaceSchema,
+ user,
+} from "@openstatus/db/src/schema";
+import {
+ addUserToWorkspace,
+ createTestWorkspace,
+ createUser,
+} from "@openstatus/db/src/test/factories";
import { expect } from "@std/expect";
import { afterEach, beforeEach, describe, test } from "@std/testing/bdd";
import { assertSpyCalls, type Stub, stub } from "@std/testing/mock";
import type Stripe from "stripe";
+import { createInnerTRPCContext } from "../../trpc";
+import { workspaceRouter } from "../workspace";
+import { stripeRouter } from "./index";
import { stripe } from "./shared";
-import { TRIAL_DAYS, maybeStartSignupTrial } from "./trial";
+import { TRIAL_DAYS } from "./trial";
import { PLANS } from "./utils";
const STARTER_PRICE = PLANS.find((p) => p.plan === "starter")?.price.monthly
.priceIds.test;
-const trialEnd = Math.floor(Date.now() / 1000) + TRIAL_DAYS * 86_400;
-async function freeWorkspace() {
- return createTestWorkspace({ plan: "free", stripeId: null });
+type Fixture = Awaited>;
+
+function asCaller(fixture: Fixture, userId = fixture.user.id) {
+ const ws = selectWorkspaceSchema.parse(fixture.workspace);
+ return stripeRouter.createCaller(
+ createInnerTRPCContext({
+ session: { user: { id: String(userId) } },
+ workspace: ws,
+ user: selectUserSchema.parse({ ...fixture.user, id: userId }),
+ }),
+ );
+}
+
+async function link(userId: number, provider: string) {
+ await db.insert(account).values({
+ userId,
+ type: "oauth",
+ provider,
+ providerAccountId: crypto.randomUUID(),
+ });
+}
+
+async function freeWorkspace(
+ overrides: Parameters[0] = {},
+ providers: string[] = ["github"],
+) {
+ const fixture = await createTestWorkspace({
+ plan: "free",
+ stripeId: null,
+ subscriptionId: null,
+ trialEndsAt: null,
+ ...overrides,
+ });
+ for (const provider of providers) await link(fixture.user.id, provider);
+ return fixture;
}
function customer(id: string, metadata: Record = {}) {
return { id, metadata } as unknown as Stripe.Customer;
}
-describe("maybeStartSignupTrial", () => {
+function trialInput(fixture: Fixture, extra: Record = {}) {
+ return {
+ trial: true,
+ plan: "starter" as const,
+ interval: "monthly" as const,
+ currency: "USD",
+ workspaceSlug: fixture.workspace.slug,
+ ...extra,
+ };
+}
+
+describe("trial checkout", () => {
let customerId: string;
let customers: Stripe.Customer[];
- let price: Partial;
+ let subscriptions: Partial[];
let stubs: Stub[];
- let list: Stub;
+ let listCustomers: Stub;
let createCustomer: Stub;
- let createSubscription: Stub;
- // Runs inside `customers.create`, to interleave work with the Stripe call.
- let onCreateCustomer: (() => Promise) | undefined;
+ let createSession: Stub;
beforeEach(() => {
customerId = `cus_${crypto.randomUUID()}`;
customers = [];
- onCreateCustomer = undefined;
- price = { currency: "usd", currency_options: {} };
- list = stub(
+ subscriptions = [];
+ listCustomers = stub(
stripe.customers,
"list",
() =>
@@ -47,32 +98,26 @@ describe("maybeStartSignupTrial", () => {
Stripe.ApiList
>) as Stripe.ApiListPromise,
);
- createCustomer = stub(stripe.customers, "create", async () => {
- await onCreateCustomer?.();
- return { id: customerId } as Stripe.Response;
- });
- createSubscription = stub(stripe.subscriptions, "create", () =>
+ createCustomer = stub(stripe.customers, "create", () =>
+ Promise.resolve({ id: customerId } as Stripe.Response),
+ );
+ createSession = stub(stripe.checkout.sessions, "create", () =>
Promise.resolve({
- id: "sub_trial",
- status: "trialing",
- trial_end: trialEnd,
- items: {
- data: [
- {
- price: { id: STARTER_PRICE },
- quantity: 1,
- current_period_end: trialEnd,
- },
- ],
- },
- } as Stripe.Response),
+ url: "https://checkout.stripe.test/session",
+ } as Stripe.Response),
);
stubs = [
- list,
+ listCustomers,
createCustomer,
- createSubscription,
- stub(stripe.prices, "retrieve", () =>
- Promise.resolve(price as Stripe.Response),
+ createSession,
+ stub(
+ stripe.subscriptions,
+ "list",
+ // Awaited for the eligibility scan, auto-paged for the live lookup.
+ () =>
+ Object.assign(Promise.resolve({ data: subscriptions }), {
+ autoPagingToArray: () => Promise.resolve([]),
+ }) as unknown as Stripe.ApiListPromise,
),
];
});
@@ -81,210 +126,256 @@ describe("maybeStartSignupTrial", () => {
for (const s of stubs) s.restore();
});
- test("starts a Starter trial on the owner's workspace", async () => {
- const { workspace: ws, user } = await freeWorkspace();
- price = {
- currency: "usd",
- currency_options: {
- eur: {} as Stripe.Price.CurrencyOptions,
- },
- };
-
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- provider: "github",
- currency: "EUR",
+ describe("getCheckoutSession", () => {
+ test("opens a card-less Starter trial checkout", async () => {
+ const f = await freeWorkspace();
+
+ const result = await asCaller(f).getCheckoutSession(
+ trialInput(f, { surface: "billing", intent: " Slack alerts " }),
+ );
+
+ expect(result?.type).toBe("checkout");
+ assertSpyCalls(createCustomer, 1);
+ assertSpyCalls(createSession, 1);
+ const params = createSession.calls[0]?.args[0];
+ expect(params).toMatchObject({
+ mode: "subscription",
+ customer: customerId,
+ line_items: [{ price: STARTER_PRICE, quantity: 1 }],
+ payment_method_collection: "if_required",
+ subscription_data: {
+ trial_period_days: TRIAL_DAYS,
+ trial_settings: {
+ end_behavior: { missing_payment_method: "cancel" },
+ },
+ metadata: {
+ source: "trial_opt_in",
+ surface: "billing",
+ intent: "Slack alerts",
+ },
+ },
+ });
+ expect(params?.success_url).toContain("?trial=true");
});
- const trialEndsAt = new Date(trialEnd * 1000);
- expect(result).toEqual({ started: true, trialEndsAt });
-
- assertSpyCalls(createCustomer, 1);
- expect(createCustomer.calls[0]?.args).toEqual([
- {
- email: user.email,
- metadata: { workspaceId: String(ws.id), trialed: "true" },
- },
- { idempotencyKey: `trial-customer:ws_${ws.id}` },
- ]);
-
- assertSpyCalls(createSubscription, 1);
- const [params, options] = createSubscription.calls[0]?.args ?? [];
- expect(params).toMatchObject({
- customer: customerId,
- currency: "eur",
- items: [{ price: STARTER_PRICE }],
- trial_period_days: TRIAL_DAYS,
- trial_settings: { end_behavior: { missing_payment_method: "cancel" } },
- });
- expect(options).toEqual({ idempotencyKey: `trial-sub:ws_${ws.id}` });
-
- const after = await db
- .select()
- .from(workspace)
- .where(eq(workspace.id, ws.id))
- .get();
- expect(after?.plan).toBe("starter");
- expect(after?.stripeId).toBe(customerId);
- expect(after?.subscriptionId).toBe("sub_trial");
- expect(after?.trialEndsAt).toEqual(trialEndsAt);
- expect(after?.endsAt).toEqual(trialEndsAt);
- expect(JSON.parse(after?.limits ?? "{}")).toEqual(getLimits("starter"));
- });
+ test("omits an empty intent and surface", async () => {
+ const f = await freeWorkspace();
- test("falls back to the price currency when the requested one is missing", async () => {
- const { user } = await freeWorkspace();
- price = { currency: "eur", currency_options: {} };
+ await asCaller(f).getCheckoutSession(trialInput(f, { intent: " " }));
- await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- currency: "USD",
+ expect(
+ createSession.calls[0]?.args[0]?.subscription_data?.metadata,
+ ).toEqual({ source: "trial_opt_in" });
});
- expect(createSubscription.calls[0]?.args[0]).toMatchObject({
- currency: "eur",
- });
- });
+ test("a regular checkout carries no trial parameters", async () => {
+ const f = await freeWorkspace();
- test("skips SSO sign-ins", async () => {
- const { user } = await freeWorkspace();
+ await asCaller(f).getCheckoutSession({
+ currency: "USD",
+ workspaceSlug: f.workspace.slug,
+ plan: "team",
+ });
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- provider: "workos",
- currency: "USD",
+ const params = createSession.calls[0]?.args[0];
+ expect(params?.payment_method_collection).toBeUndefined();
+ expect(params?.subscription_data).toBeUndefined();
+ expect(params?.success_url).toContain("?success=true");
});
- expect(result).toEqual({ started: false, reason: "sso" });
- assertSpyCalls(createCustomer, 0);
- });
+ test("reuses the customer an abandoned checkout left behind", async () => {
+ const stripeId = `cus_${crypto.randomUUID()}`;
+ const f = await freeWorkspace({ stripeId });
+ subscriptions = [{ status: "incomplete_expired" }];
- test("skips magic-link sign-ins", async () => {
- const { user } = await freeWorkspace();
+ const result = await asCaller(f).getCheckoutSession(trialInput(f));
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- provider: "resend",
- currency: "USD",
+ expect(result?.type).toBe("checkout");
+ assertSpyCalls(createCustomer, 0);
+ expect(createSession.calls[0]?.args[0]).toMatchObject({
+ customer: stripeId,
+ });
});
- expect(result).toEqual({ started: false, reason: "email" });
- assertSpyCalls(createCustomer, 0);
- });
+ test("refuses a customer that ever had a subscription", async () => {
+ const f = await freeWorkspace({ stripeId: `cus_${crypto.randomUUID()}` });
+ subscriptions = [{ status: "canceled" }];
+
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({
+ code: "PRECONDITION_FAILED",
+ message: "This workspace has already had a subscription.",
+ });
+ assertSpyCalls(createSession, 0);
+ });
- test("skips users with a pending invitation", async () => {
- const { workspace: ws, user } = await freeWorkspace();
- await db.insert(invitation).values({
- email: (user.email ?? "").toUpperCase(),
- workspaceId: ws.id,
- token: crypto.randomUUID(),
- expiresAt: new Date(Date.now() + 86_400_000),
+ test("refuses a magic-link-only account", async () => {
+ const f = await freeWorkspace({}, ["resend"]);
+
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({
+ code: "PRECONDITION_FAILED",
+ message: "Sign in with GitHub or Google to start a trial.",
+ });
+ assertSpyCalls(listCustomers, 0);
+ assertSpyCalls(createCustomer, 0);
});
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- currency: "USD",
+ test("refuses an SSO-only account", async () => {
+ const f = await freeWorkspace({}, ["workos"]);
+
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({
+ code: "PRECONDITION_FAILED",
+ message: "Trials are not available for SSO accounts.",
+ });
+ assertSpyCalls(createCustomer, 0);
});
- expect(result).toEqual({ started: false, reason: "invited" });
- assertSpyCalls(createCustomer, 0);
- });
+ test("an account linked to both GitHub and a magic link qualifies", async () => {
+ const f = await freeWorkspace({}, ["resend", "github"]);
- test("skips disposable email domains", async () => {
- const { user } = await freeWorkspace();
+ const result = await asCaller(f).getCheckoutSession(trialInput(f));
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: "someone@mailinator.com",
- currency: "USD",
+ expect(result?.type).toBe("checkout");
});
- expect(result).toEqual({ started: false, reason: "disposable" });
- assertSpyCalls(list, 0);
- });
-
- test("skips emails that already had a trial", async () => {
- const { user } = await freeWorkspace();
- customers = [
- customer("cus_paid"),
- customer("cus_old", { trialed: "true" }),
- ];
+ test("refuses a disposable email", async () => {
+ const f = await freeWorkspace();
+ await db
+ .update(user)
+ .set({ email: `someone-${f.user.id}@mailinator.com` })
+ .where(eq(user.id, f.user.id));
+
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" });
+ assertSpyCalls(listCustomers, 0);
+ });
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: "O'Brien@example.com",
- currency: "USD",
+ test("refuses an email that already had a trial", async () => {
+ const f = await freeWorkspace();
+ customers = [
+ customer("cus_paid"),
+ customer("cus_old", { trialed: "true" }),
+ ];
+
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({
+ code: "PRECONDITION_FAILED",
+ message: "This workspace or email has already used a trial.",
+ });
+ expect(listCustomers.calls[0]?.args[0]).toEqual({
+ email: f.user.email,
+ limit: 100,
+ });
+ assertSpyCalls(createCustomer, 0);
});
- expect(result).toEqual({ started: false, reason: "already_trialed" });
- expect(list.calls[0]?.args[0]).toEqual({
- email: "o'brien@example.com",
- limit: 100,
+ test("checks both spellings of a mixed-case email", async () => {
+ const f = await freeWorkspace();
+ const email = `Someone-${f.user.id}@Example.com`;
+ await db.update(user).set({ email }).where(eq(user.id, f.user.id));
+
+ await asCaller(f).getCheckoutSession(trialInput(f));
+
+ expect(listCustomers.calls.map((c) => c.args[0])).toEqual([
+ { email, limit: 100 },
+ { email: email.toLowerCase(), limit: 100 },
+ ]);
});
- assertSpyCalls(createCustomer, 0);
- });
- test("ignores customers for the email that never trialed", async () => {
- const { user } = await freeWorkspace();
- customers = [customer("cus_paid")];
+ test("refuses a workspace that already trialed, before asking Stripe", async () => {
+ const f = await freeWorkspace({ trialedAt: new Date() });
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- currency: "USD",
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({
+ code: "PRECONDITION_FAILED",
+ message: "This workspace or email has already used a trial.",
+ });
+ assertSpyCalls(listCustomers, 0);
});
- expect(result).toMatchObject({ started: true });
- assertSpyCalls(createCustomer, 1);
- });
+ test("refuses a member who is not the owner", async () => {
+ const f = await freeWorkspace();
+ const member = await createUser();
+ await addUserToWorkspace(member.id, f.workspace.id, "member");
+ await link(member.id, "github");
+
+ await expect(
+ asCaller(f, member.id).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({
+ code: "PRECONDITION_FAILED",
+ message: "Only the owner of a free workspace can start a trial.",
+ });
+ });
- test("drops its customer when another request linked one first", async () => {
- const { workspace: ws, user } = await freeWorkspace();
- const linked = `cus_${crypto.randomUUID()}`;
- // Simulate the race: a checkout links its customer while ours is created.
- onCreateCustomer = async () => {
- await db
- .update(workspace)
- .set({ stripeId: linked })
- .where(eq(workspace.id, ws.id));
- };
- const delCustomer = stub(stripe.customers, "del", () =>
- Promise.resolve({} as Stripe.Response),
- );
- stubs.push(delCustomer);
+ test("refuses a paid workspace", async () => {
+ const f = await freeWorkspace({ plan: "team" });
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- currency: "USD",
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f)),
+ ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" });
+ assertSpyCalls(createCustomer, 0);
});
- expect(result).toEqual({ started: false, reason: "not_eligible" });
- expect(delCustomer.calls[0]?.args[0]).toBe(customerId);
- assertSpyCalls(createSubscription, 0);
- const after = await db
- .select({ stripeId: workspace.stripeId })
- .from(workspace)
- .where(eq(workspace.id, ws.id))
- .get();
- expect(after?.stripeId).toBe(linked);
+ test("refuses a trial on any plan but Starter monthly", async () => {
+ const f = await freeWorkspace();
+
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f, { plan: "team" })),
+ ).rejects.toMatchObject({ code: "BAD_REQUEST" });
+ await expect(
+ asCaller(f).getCheckoutSession(trialInput(f, { interval: "yearly" })),
+ ).rejects.toMatchObject({ code: "BAD_REQUEST" });
+ assertSpyCalls(createSession, 0);
+ });
});
- test("leaves an already-billed workspace alone", async () => {
- const { user } = await createTestWorkspace({ plan: "team" });
+ describe("workspace.get trial", () => {
+ function getWorkspace(fixture: Fixture) {
+ return workspaceRouter
+ .createCaller(
+ createInnerTRPCContext({
+ session: { user: { id: String(fixture.user.id) } },
+ workspace: selectWorkspaceSchema.parse(fixture.workspace),
+ user: selectUserSchema.parse(fixture.user),
+ }),
+ )
+ .get();
+ }
+
+ test("eligible for an owner signed in with GitHub, without Stripe", async () => {
+ const f = await freeWorkspace();
+ customers = [customer("cus_old", { trialed: "true" })];
+
+ expect((await getWorkspace(f)).trial).toEqual({
+ eligible: true,
+ });
+ assertSpyCalls(listCustomers, 0);
+ });
+
+ test("not eligible once the workspace has trialed", async () => {
+ const f = await freeWorkspace({ trialedAt: new Date() });
- const result = await maybeStartSignupTrial({
- userId: user.id,
- email: user.email ?? "",
- currency: "USD",
+ expect((await getWorkspace(f)).trial).toEqual({
+ eligible: false,
+ reason: "already_trialed",
+ });
});
- expect(result).toEqual({ started: false, reason: "not_eligible" });
- assertSpyCalls(createCustomer, 0);
+ test("names the reason otherwise", async () => {
+ const f = await freeWorkspace({}, ["resend"]);
+
+ expect((await getWorkspace(f)).trial).toEqual({
+ eligible: false,
+ reason: "email",
+ });
+ });
});
});
diff --git a/packages/api/src/router/stripe/trial.ts b/packages/api/src/router/stripe/trial.ts
index 51cb1ec28..e4f39a77f 100644
--- a/packages/api/src/router/stripe/trial.ts
+++ b/packages/api/src/router/stripe/trial.ts
@@ -1,158 +1,145 @@
-import {
- ConflictError,
- type DB,
- type ServiceContext,
-} from "@openstatus/services";
-import { hasPendingInvitation } from "@openstatus/services/invitation";
+import type { DB, ServiceContext } from "@openstatus/services";
+import { listUserProviders } from "@openstatus/services/user";
import {
downgradeWorkspaceToFree,
- findTrialEligibleWorkspace,
+ isTrialEligibleWorkspace,
listOwnedTrialWorkspaces,
- updateWorkspacePlan,
- updateWorkspaceStripeId,
} from "@openstatus/services/workspace";
import MailChecker from "mailchecker";
+import type Stripe from "stripe";
import { env } from "../../env";
-import {
- buildFromSubscriptionOrThrow,
- cancelSubscription,
- stripe,
-} from "./shared";
-import { getPriceIdForPlan } from "./utils";
+import { cancelSubscription, stripe } from "./shared";
+import { TRIAL_DAYS, type TrialSurface } from "./trial-config";
+
+export { TRIAL_DAYS, type TrialSurface };
-export const TRIAL_DAYS = 14;
+// A magic link proves only inbox access and SSO members are provisioned by
+// their org, so only these sign-ins can open a trial.
+const TRIAL_PROVIDERS = ["github", "google"];
export type TrialSkipReason =
| "disabled"
| "sso"
| "email"
- | "invited"
| "disposable"
| "already_trialed"
- | "not_eligible"
- | "stripe_error";
+ | "already_subscribed"
+ | "not_eligible";
-export type SignupTrialResult =
- | { started: true; trialEndsAt: Date }
- | { started: false; reason: TrialSkipReason };
+export type TrialEligibility =
+ | { eligible: true }
+ | { eligible: false; reason: TrialSkipReason };
// `customers.list` is read-after-write consistent; the Search API is not, so
-// a re-signup right after a trial could slip past it.
+// a re-signup right after a trial could slip past it. Its email filter is
+// case-sensitive and customers keep the email as stored, so both spellings
+// are checked.
async function hasTrialedBefore(email: string) {
- const result = await stripe.customers.list({ email, limit: 100 });
- return result.data.some((c) => c.metadata.trialed === "true");
+ const emails = [...new Set([email, email.toLowerCase()])];
+ const results = await Promise.all(
+ emails.map((e) => stripe.customers.list({ email: e, limit: 100 })),
+ );
+ return results.some((r) => r.data.some((c) => c.metadata.trialed === "true"));
}
-async function resolveCurrency(priceId: string, requested: "USD" | "EUR") {
- const price = await stripe.prices.retrieve(priceId, {
- expand: ["currency_options"],
+// `incomplete` is what an abandoned checkout leaves behind; it never entitled
+// anything and must not burn the trial.
+async function hasEverSubscribed(customerId: string) {
+ const result = await stripe.subscriptions.list({
+ customer: customerId,
+ status: "all",
+ limit: 100,
});
- const wanted = requested.toLowerCase();
- return wanted === price.currency || price.currency_options?.[wanted]
- ? wanted
- : price.currency;
+ return result.data.some(
+ (sub) => sub.status !== "incomplete" && sub.status !== "incomplete_expired",
+ );
}
-export async function maybeStartSignupTrial(args: {
- userId: number;
- email: string;
- provider?: string;
- currency: "USD" | "EUR";
- db?: DB;
-}): Promise {
- const { db } = args;
- const email = args.email.trim().toLowerCase();
+/**
+ * Cheapest first: the workspace row, then DB reads, then Stripe. `stripe:
+ * false` skips the Stripe guards, which only catch trials from before
+ * `trialedAt` and ex-subscribers; the Checkout mutation always runs them.
+ */
+export async function checkTrialEligibility(args: {
+ ctx: ServiceContext;
+ email: string | null;
+ stripe?: boolean;
+}): Promise {
+ const { ctx } = args;
+ const email = args.email?.trim() ?? "";
+
+ if (env.SELF_HOST || !env.STRIPE_SECRET_KEY) {
+ return { eligible: false, reason: "disabled" };
+ }
- if (
- env.SELF_HOST ||
- env.NODE_ENV === "development" ||
- !env.STRIPE_SECRET_KEY
- ) {
- return { started: false, reason: "disabled" };
+ const userId = "userId" in ctx.actor ? ctx.actor.userId : undefined;
+ if (!userId) return { eligible: false, reason: "not_eligible" };
+
+ const ws = ctx.workspace;
+ if (ws.trialedAt) return { eligible: false, reason: "already_trialed" };
+ // A fresh signup workspace has no plan column yet — that is free.
+ if ((ws.plan && ws.plan !== "free") || ws.subscriptionId || ws.trialEndsAt) {
+ return { eligible: false, reason: "not_eligible" };
}
- if (args.provider === "workos") return { started: false, reason: "sso" };
- // A magic link proves only inbox access, so email signups start on free and
- // upgrade through checkout.
- if (args.provider === "resend") return { started: false, reason: "email" };
- if (await hasPendingInvitation({ email, db })) {
- return { started: false, reason: "invited" };
+
+ const providers = await listUserProviders({ input: { userId }, db: ctx.db });
+ if (!providers.some((p) => TRIAL_PROVIDERS.includes(p))) {
+ const sso = providers.includes("workos") || providers.includes("oidc");
+ return { eligible: false, reason: sso ? "sso" : "email" };
}
- if (!MailChecker.isValid(email)) {
- return { started: false, reason: "disposable" };
+ if (!email || !MailChecker.isValid(email.toLowerCase())) {
+ return { eligible: false, reason: "disposable" };
}
+ if (!(await isTrialEligibleWorkspace({ ctx }))) {
+ return { eligible: false, reason: "not_eligible" };
+ }
+ if (args.stripe === false) return { eligible: true };
if (await hasTrialedBefore(email)) {
- return { started: false, reason: "already_trialed" };
+ return { eligible: false, reason: "already_trialed" };
}
-
- const ws = await findTrialEligibleWorkspace({
- input: { userId: args.userId },
- db,
- });
- if (!ws) return { started: false, reason: "not_eligible" };
-
- const ctx: ServiceContext = {
- workspace: ws,
- actor: { type: "system", job: "signup-trial" },
- db,
- };
-
- const priceId = getPriceIdForPlan("starter", "monthly");
- if (!priceId) throw new Error("Missing Starter monthly price");
- const currency = await resolveCurrency(priceId, args.currency);
-
- const customer = await stripe.customers.create(
- { email, metadata: { workspaceId: String(ws.id), trialed: "true" } },
- { idempotencyKey: `trial-customer:ws_${ws.id}` },
- );
-
- try {
- await updateWorkspaceStripeId({ ctx, input: { stripeId: customer.id } });
- } catch (err) {
- if (!(err instanceof ConflictError)) throw err;
- // Another request (a checkout, say) linked a customer since the
- // eligibility read. Drop ours so its `trialed` flag doesn't block a
- // later trial for this email.
- await stripe.customers.del(customer.id).catch(() => undefined);
- return { started: false, reason: "not_eligible" };
+ if (
+ ctx.workspace.stripeId &&
+ (await hasEverSubscribed(ctx.workspace.stripeId))
+ ) {
+ return { eligible: false, reason: "already_subscribed" };
}
- const subscription = await stripe.subscriptions.create(
- {
- customer: customer.id,
- currency,
- items: [{ price: priceId }],
+ return { eligible: true };
+}
+
+export const TRIAL_SKIP_MESSAGE: Record = {
+ disabled: "Trials are not available on this installation.",
+ sso: "Trials are not available for SSO accounts.",
+ email: "Sign in with GitHub or Google to start a trial.",
+ disposable: "Trials are not available for this email address.",
+ already_trialed: "This workspace or email has already used a trial.",
+ already_subscribed: "This workspace has already had a subscription.",
+ not_eligible: "Only the owner of a free workspace can start a trial.",
+};
+
+/** The Checkout fields that turn a Starter subscription into a card-less trial. */
+export function trialCheckoutParams(args: {
+ surface?: TrialSurface;
+ intent?: string;
+}): Pick<
+ Stripe.Checkout.SessionCreateParams,
+ "payment_method_collection" | "subscription_data"
+> {
+ return {
+ payment_method_collection: "if_required",
+ subscription_data: {
trial_period_days: TRIAL_DAYS,
trial_settings: {
end_behavior: { missing_payment_method: "cancel" },
},
- payment_settings: { save_default_payment_method: "on_subscription" },
- metadata: { source: "signup_trial", workspaceId: String(ws.id) },
- },
- { idempotencyKey: `trial-sub:ws_${ws.id}` },
- );
-
- const built = buildFromSubscriptionOrThrow(subscription);
- if (!built || !subscription.trial_end) {
- throw new Error(`Trial subscription ${subscription.id} is not a trial`);
- }
-
- const trialEndsAt = new Date(subscription.trial_end * 1000);
-
- await updateWorkspacePlan({
- ctx,
- input: {
- plan: built.plan,
- subscriptionId: subscription.id,
- endsAt: trialEndsAt,
- paidUntil: trialEndsAt,
- trialEndsAt,
- limits: built.limits,
- reason: "trial_started",
+ metadata: {
+ source: "trial_opt_in",
+ ...(args.surface && { surface: args.surface }),
+ ...(args.intent && { intent: args.intent }),
+ },
},
- });
-
- return { started: true, trialEndsAt };
+ };
}
export async function cancelOwnedTrials(args: {
diff --git a/packages/api/src/router/stripe/webhook.ts b/packages/api/src/router/stripe/webhook.ts
index 486f39716..db9a5d5a8 100644
--- a/packages/api/src/router/stripe/webhook.ts
+++ b/packages/api/src/router/stripe/webhook.ts
@@ -15,6 +15,7 @@ import {
downgradeWorkspaceToFree,
getWorkspaceByStripeId,
listWorkspaceOwners,
+ markWorkspaceTrialed,
previewWorkspaceDowngrade,
updateWorkspacePlan,
} from "@openstatus/services/workspace";
@@ -470,12 +471,16 @@ export const webhookRouter = createTRPCRouter({
// for an unrelated `customer.subscription.updated` to come along.
await cancelSupersededSubscriptions(live, subscription);
+ // Not the live status: a late delivery can land after the trial ended.
+ const isTrial = subscription.metadata.source === "trial_opt_in";
+ const wsCtx: ServiceContext = {
+ workspace: ws,
+ actor: { type: "system", job: "stripe-session-completed" },
+ db: opts.ctx.db,
+ };
+
await updateWorkspacePlan({
- ctx: {
- workspace: ws,
- actor: { type: "system", job: "stripe-session-completed" },
- db: opts.ctx.db,
- },
+ ctx: wsCtx,
input: {
plan: built.plan,
subscriptionId: subscription.id,
@@ -483,10 +488,34 @@ export const webhookRouter = createTRPCRouter({
paidUntil: getCurrentPeriodEnd(subscription),
trialEndsAt: trialEndsAtOf(subscription),
limits: syncedLimits(subscription, built),
- reason: "checkout_session_completed",
+ reason: isTrial ? "trial_started" : "checkout_session_completed",
},
});
+ if (isTrial) {
+ // Stamped on completion, not when the Checkout opens, so an abandoned
+ // page does not burn the trial.
+ await stripe.customers.update(customerId, {
+ metadata: { trialed: "true" },
+ });
+ const firstMark = await markWorkspaceTrialed({ ctx: wsCtx });
+ // A redelivered event must not count the trial twice.
+ if (firstMark) {
+ await trackForWorkspaceOwner({
+ db: opts.ctx.db,
+ ws: { ...ws, plan: built.plan },
+ event: Events.StartTrial,
+ props: {
+ surface: subscription.metadata.surface,
+ intent: subscription.metadata.intent,
+ trialEndsAt: trialEndsAtOf(subscription)?.toISOString(),
+ ...billingProps(subscription),
+ },
+ });
+ }
+ return;
+ }
+
await trackForWorkspaceOwner({
db: opts.ctx.db,
ws: { ...ws, plan: built.plan },
@@ -494,7 +523,6 @@ export const webhookRouter = createTRPCRouter({
props: {
fromPlan: ws.plan,
toPlan: built.plan,
- trialing: subscription.status === "trialing",
...billingProps(subscription),
},
});
diff --git a/packages/api/src/router/workspace.ts b/packages/api/src/router/workspace.ts
index 585d6e1b3..f3e3d21e4 100644
--- a/packages/api/src/router/workspace.ts
+++ b/packages/api/src/router/workspace.ts
@@ -9,15 +9,23 @@ import { z } from "zod";
import { toServiceCtx, toTRPCError } from "../service-adapter";
import { createTRPCRouter, protectedProcedure } from "../trpc";
+import { checkTrialEligibility } from "./stripe/trial";
export const workspaceRouter = createTRPCRouter({
// The authed middleware already resolved and parsed this row; re-selecting
// it would be the same query. Counts live in `usage` — the shell reads
// `limits` on every route, the counts only on two surfaces.
// `trialDaysLeft` is derived here, not stored: it depends on "now".
- get: protectedProcedure.query(({ ctx }) => ({
+ // `trial` skips Stripe and costs no read for a non-free
+ // workspace; the Checkout mutation re-checks with Stripe.
+ get: protectedProcedure.query(async ({ ctx }) => ({
...ctx.workspace,
trialDaysLeft: getTrialDaysLeft(ctx.workspace.trialEndsAt),
+ trial: await checkTrialEligibility({
+ ctx: toServiceCtx(ctx),
+ email: ctx.user.email,
+ stripe: false,
+ }),
features: enabledFeatures(ctx.workspace),
})),
diff --git a/packages/db/drizzle/0093_wonderful_red_skull.sql b/packages/db/drizzle/0093_wonderful_red_skull.sql
new file mode 100644
index 000000000..cee8a1fec
--- /dev/null
+++ b/packages/db/drizzle/0093_wonderful_red_skull.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `workspace` ADD `trialed_at` integer;--> statement-breakpoint
+CREATE INDEX `account_user_id_idx` ON `account` (`user_id`);
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/0093_snapshot.json b/packages/db/drizzle/meta/0093_snapshot.json
new file mode 100644
index 000000000..6e55283d9
--- /dev/null
+++ b/packages/db/drizzle/meta/0093_snapshot.json
@@ -0,0 +1,6907 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "29a4b4ca-df88-4161-a2aa-7aba5e6403b6",
+ "prevId": "94dd2640-d257-4088-ac88-7cb99df6d1b7",
+ "tables": {
+ "workspace": {
+ "name": "workspace",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripe_id": {
+ "name": "stripe_id",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ends_at": {
+ "name": "ends_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "paid_until": {
+ "name": "paid_until",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "trial_ends_at": {
+ "name": "trial_ends_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "trialed_at": {
+ "name": "trialed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "limits": {
+ "name": "limits",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{}'"
+ },
+ "workos_organization_id": {
+ "name": "workos_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sso_enabled": {
+ "name": "sso_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "dsn": {
+ "name": "dsn",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "workspace_slug_unique": {
+ "name": "workspace_slug_unique",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ },
+ "workspace_stripe_id_unique": {
+ "name": "workspace_stripe_id_unique",
+ "columns": [
+ "stripe_id"
+ ],
+ "isUnique": true
+ },
+ "workspace_workos_organization_id_unique": {
+ "name": "workspace_workos_organization_id_unique",
+ "columns": [
+ "workos_organization_id"
+ ],
+ "isUnique": true
+ },
+ "workspace_id_dsn_unique": {
+ "name": "workspace_id_dsn_unique",
+ "columns": [
+ "id",
+ "dsn"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "workspace_sso_domain": {
+ "name": "workspace_sso_domain",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "verified_at": {
+ "name": "verified_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "workspace_sso_domain_domain_unique": {
+ "name": "workspace_sso_domain_domain_unique",
+ "columns": [
+ "domain"
+ ],
+ "isUnique": true
+ },
+ "workspace_sso_domain_workspace_id_idx": {
+ "name": "workspace_sso_domain_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "workspace_sso_domain_workspace_id_workspace_id_fk": {
+ "name": "workspace_sso_domain_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_sso_domain",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "account": {
+ "name": "account",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_provider_account_id_pk": {
+ "columns": [
+ "provider",
+ "provider_account_id"
+ ],
+ "name": "account_provider_provider_account_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "photo_url": {
+ "name": "photo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_tenant_id_unique": {
+ "name": "user_tenant_id_unique",
+ "columns": [
+ "tenant_id"
+ ],
+ "isUnique": true
+ },
+ "user_email_idx": {
+ "name": "user_email_idx",
+ "columns": [
+ "email"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_to_workspaces": {
+ "name": "users_to_workspaces",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "users_to_workspaces_workspace_id_idx": {
+ "name": "users_to_workspaces_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "users_to_workspaces_user_id_user_id_fk": {
+ "name": "users_to_workspaces_user_id_user_id_fk",
+ "tableFrom": "users_to_workspaces",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "users_to_workspaces_workspace_id_workspace_id_fk": {
+ "name": "users_to_workspaces_workspace_id_workspace_id_fk",
+ "tableFrom": "users_to_workspaces",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "users_to_workspaces_user_id_workspace_id_pk": {
+ "columns": [
+ "user_id",
+ "workspace_id"
+ ],
+ "name": "users_to_workspaces_user_id_workspace_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verification_token": {
+ "name": "verification_token",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verification_token_identifier_token_pk": {
+ "columns": [
+ "identifier",
+ "token"
+ ],
+ "name": "verification_token_identifier_token_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "status_report": {
+ "name": "status_report",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "status_report_workspace_created_idx": {
+ "name": "status_report_workspace_created_idx",
+ "columns": [
+ "workspace_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "status_report_page_id_idx": {
+ "name": "status_report_page_id_idx",
+ "columns": [
+ "page_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "status_report_workspace_id_workspace_id_fk": {
+ "name": "status_report_workspace_id_workspace_id_fk",
+ "tableFrom": "status_report",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "status_report_page_id_page_id_fk": {
+ "name": "status_report_page_id_page_id_fk",
+ "tableFrom": "status_report",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "status_report_created_by_user_id_fk": {
+ "name": "status_report_created_by_user_id_fk",
+ "tableFrom": "status_report",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "status_report_updated_by_user_id_fk": {
+ "name": "status_report_updated_by_user_id_fk",
+ "tableFrom": "status_report",
+ "tableTo": "user",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "status_report_update": {
+ "name": "status_report_update",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "date": {
+ "name": "date",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status_report_id": {
+ "name": "status_report_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "status_report_update_status_report_id_idx": {
+ "name": "status_report_update_status_report_id_idx",
+ "columns": [
+ "status_report_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "status_report_update_status_report_id_status_report_id_fk": {
+ "name": "status_report_update_status_report_id_status_report_id_fk",
+ "tableFrom": "status_report_update",
+ "tableTo": "status_report",
+ "columnsFrom": [
+ "status_report_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "status_report_update_created_by_user_id_fk": {
+ "name": "status_report_update_created_by_user_id_fk",
+ "tableFrom": "status_report_update",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "status_report_update_updated_by_user_id_fk": {
+ "name": "status_report_update_updated_by_user_id_fk",
+ "tableFrom": "status_report_update",
+ "tableTo": "user",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integration": {
+ "name": "integration",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "credential": {
+ "name": "credential",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "integration_workspace_id_idx": {
+ "name": "integration_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "integration_workspace_id_workspace_id_fk": {
+ "name": "integration_workspace_id_workspace_id_fk",
+ "tableFrom": "integration",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "page": {
+ "name": "page",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "custom_domain": {
+ "name": "custom_domain",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "published": {
+ "name": "published",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "force_theme": {
+ "name": "force_theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'system'"
+ },
+ "custom_theme": {
+ "name": "custom_theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_protected": {
+ "name": "password_protected",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "access_type": {
+ "name": "access_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'public'"
+ },
+ "auth_email_domains": {
+ "name": "auth_email_domains",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allowed_ip_ranges": {
+ "name": "allowed_ip_ranges",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "homepage_url": {
+ "name": "homepage_url",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "contact_url": {
+ "name": "contact_url",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "default_locale": {
+ "name": "default_locale",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'en'"
+ },
+ "locales": {
+ "name": "locales",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "legacy_page": {
+ "name": "legacy_page",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "configuration": {
+ "name": "configuration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allow_index": {
+ "name": "allow_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "show_monitor_values": {
+ "name": "show_monitor_values",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "page_slug_unique": {
+ "name": "page_slug_unique",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ },
+ "page_lower_slug_idx": {
+ "name": "page_lower_slug_idx",
+ "columns": [
+ "LOWER(\"slug\")"
+ ],
+ "isUnique": false
+ },
+ "page_lower_custom_domain_idx": {
+ "name": "page_lower_custom_domain_idx",
+ "columns": [
+ "LOWER(\"custom_domain\")"
+ ],
+ "isUnique": false
+ },
+ "page_workspace_id_idx": {
+ "name": "page_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "page_workspace_id_workspace_id_fk": {
+ "name": "page_workspace_id_workspace_id_fk",
+ "tableFrom": "page",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor": {
+ "name": "monitor",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "job_type": {
+ "name": "job_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'http'"
+ },
+ "periodicity": {
+ "name": "periodicity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'other'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "active": {
+ "name": "active",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "regions": {
+ "name": "regions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "url": {
+ "name": "url",
+ "type": "text(2048)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "external_name": {
+ "name": "external_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "headers": {
+ "name": "headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'GET'"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "timeout": {
+ "name": "timeout",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 45000
+ },
+ "degraded_after": {
+ "name": "degraded_after",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "assertions": {
+ "name": "assertions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "otel_endpoint": {
+ "name": "otel_endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "otel_headers": {
+ "name": "otel_headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "public": {
+ "name": "public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "retry": {
+ "name": "retry",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 3
+ },
+ "follow_redirects": {
+ "name": "follow_redirects",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "grpc_service": {
+ "name": "grpc_service",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "grpc_tls": {
+ "name": "grpc_tls",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'tls'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "monitor_workspace_id_active_idx": {
+ "name": "monitor_workspace_id_active_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false,
+ "where": "\"monitor\".\"deleted_at\" IS NULL"
+ }
+ },
+ "foreignKeys": {
+ "monitor_workspace_id_workspace_id_fk": {
+ "name": "monitor_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "page_subscriber": {
+ "name": "page_subscriber",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "channel_type": {
+ "name": "channel_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'email'"
+ },
+ "webhook_url": {
+ "name": "webhook_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_config": {
+ "name": "channel_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "slack_channel_id": {
+ "name": "slack_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'self_signup'"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "unsubscribed_at": {
+ "name": "unsubscribed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "page_subscriber_page_id_idx": {
+ "name": "page_subscriber_page_id_idx",
+ "columns": [
+ "page_id"
+ ],
+ "isUnique": false
+ },
+ "idx_page_subscriber_email_page_active": {
+ "name": "idx_page_subscriber_email_page_active",
+ "columns": [
+ "LOWER(\"email\")",
+ "page_id"
+ ],
+ "isUnique": true,
+ "where": "\"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'email'"
+ },
+ "idx_page_subscriber_webhook_page_active": {
+ "name": "idx_page_subscriber_webhook_page_active",
+ "columns": [
+ "LOWER(\"webhook_url\")",
+ "page_id"
+ ],
+ "isUnique": true,
+ "where": "\"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'webhook'"
+ },
+ "idx_page_subscriber_slack_channel_page_active": {
+ "name": "idx_page_subscriber_slack_channel_page_active",
+ "columns": [
+ "slack_channel_id",
+ "page_id"
+ ],
+ "isUnique": true,
+ "where": "\"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'slack'"
+ }
+ },
+ "foreignKeys": {
+ "page_subscriber_page_id_page_id_fk": {
+ "name": "page_subscriber_page_id_page_id_fk",
+ "tableFrom": "page_subscriber",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {
+ "page_subscriber_channel_check": {
+ "name": "page_subscriber_channel_check",
+ "value": "(\"page_subscriber\".\"channel_type\" = 'email' AND \"page_subscriber\".\"email\" IS NOT NULL AND \"page_subscriber\".\"webhook_url\" IS NULL) OR (\"page_subscriber\".\"channel_type\" = 'webhook' AND \"page_subscriber\".\"webhook_url\" IS NOT NULL AND \"page_subscriber\".\"email\" IS NULL) OR (\"page_subscriber\".\"channel_type\" = 'slack' AND \"page_subscriber\".\"slack_channel_id\" IS NOT NULL AND \"page_subscriber\".\"email\" IS NULL AND \"page_subscriber\".\"webhook_url\" IS NULL)"
+ }
+ }
+ },
+ "page_subscriber_to_page_component": {
+ "name": "page_subscriber_to_page_component",
+ "columns": {
+ "page_subscriber_id": {
+ "name": "page_subscriber_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_component_id": {
+ "name": "page_component_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk": {
+ "name": "page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk",
+ "tableFrom": "page_subscriber_to_page_component",
+ "tableTo": "page_subscriber",
+ "columnsFrom": [
+ "page_subscriber_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "page_subscriber_to_page_component_page_component_id_page_component_id_fk": {
+ "name": "page_subscriber_to_page_component_page_component_id_page_component_id_fk",
+ "tableFrom": "page_subscriber_to_page_component",
+ "tableTo": "page_component",
+ "columnsFrom": [
+ "page_component_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk": {
+ "columns": [
+ "page_subscriber_id",
+ "page_component_id"
+ ],
+ "name": "page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification": {
+ "name": "notification",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{}'"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "notification_workspace_id_idx": {
+ "name": "notification_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "notification_workspace_id_workspace_id_fk": {
+ "name": "notification_workspace_id_workspace_id_fk",
+ "tableFrom": "notification",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_trigger": {
+ "name": "notification_trigger",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "notification_id_monitor_id_crontimestampe": {
+ "name": "notification_id_monitor_id_crontimestampe",
+ "columns": [
+ "notification_id",
+ "monitor_id",
+ "cron_timestamp"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "notification_trigger_monitor_id_monitor_id_fk": {
+ "name": "notification_trigger_monitor_id_monitor_id_fk",
+ "tableFrom": "notification_trigger",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_trigger_notification_id_notification_id_fk": {
+ "name": "notification_trigger_notification_id_notification_id_fk",
+ "tableFrom": "notification_trigger",
+ "tableTo": "notification",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notifications_to_monitors": {
+ "name": "notifications_to_monitors",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "notifications_to_monitors_notification_id_idx": {
+ "name": "notifications_to_monitors_notification_id_idx",
+ "columns": [
+ "notification_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "notifications_to_monitors_monitor_id_monitor_id_fk": {
+ "name": "notifications_to_monitors_monitor_id_monitor_id_fk",
+ "tableFrom": "notifications_to_monitors",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_to_monitors_notification_id_notification_id_fk": {
+ "name": "notifications_to_monitors_notification_id_notification_id_fk",
+ "tableFrom": "notifications_to_monitors",
+ "tableTo": "notification",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "notifications_to_monitors_monitor_id_notification_id_pk": {
+ "columns": [
+ "monitor_id",
+ "notification_id"
+ ],
+ "name": "notifications_to_monitors_monitor_id_notification_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_dead_letter": {
+ "name": "notification_dead_letter",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "outbox_id": {
+ "name": "outbox_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "dedup_key": {
+ "name": "dedup_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from_status": {
+ "name": "from_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "to_status": {
+ "name": "to_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "incident_id": {
+ "name": "incident_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "final_error": {
+ "name": "final_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "died_at": {
+ "name": "died_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "notification_dead_letter_dedup_key_idx": {
+ "name": "notification_dead_letter_dedup_key_idx",
+ "columns": [
+ "dedup_key"
+ ],
+ "isUnique": true
+ },
+ "notification_dead_letter_workspace_id_died_at_idx": {
+ "name": "notification_dead_letter_workspace_id_died_at_idx",
+ "columns": [
+ "workspace_id",
+ "died_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "notification_dead_letter_monitor_id_monitor_id_fk": {
+ "name": "notification_dead_letter_monitor_id_monitor_id_fk",
+ "tableFrom": "notification_dead_letter",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_dead_letter_workspace_id_workspace_id_fk": {
+ "name": "notification_dead_letter_workspace_id_workspace_id_fk",
+ "tableFrom": "notification_dead_letter",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_outbox": {
+ "name": "notification_outbox",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "dedup_key": {
+ "name": "dedup_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from_status": {
+ "name": "from_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "to_status": {
+ "name": "to_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "incident_id": {
+ "name": "incident_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "delivery_status": {
+ "name": "delivery_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "deadline_at": {
+ "name": "deadline_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "locked_by": {
+ "name": "locked_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "locked_until": {
+ "name": "locked_until",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "delivered_at": {
+ "name": "delivered_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "notification_outbox_dedup_key_idx": {
+ "name": "notification_outbox_dedup_key_idx",
+ "columns": [
+ "dedup_key"
+ ],
+ "isUnique": true
+ },
+ "notification_outbox_claim_idx": {
+ "name": "notification_outbox_claim_idx",
+ "columns": [
+ "next_attempt_at"
+ ],
+ "isUnique": false,
+ "where": "\"notification_outbox\".\"delivery_status\" = 'pending'"
+ },
+ "notification_outbox_notification_id_cron_timestamp_idx": {
+ "name": "notification_outbox_notification_id_cron_timestamp_idx",
+ "columns": [
+ "notification_id",
+ "cron_timestamp"
+ ],
+ "isUnique": false
+ },
+ "notification_outbox_channel_idx": {
+ "name": "notification_outbox_channel_idx",
+ "columns": [
+ "monitor_id",
+ "notification_id"
+ ],
+ "isUnique": false,
+ "where": "\"notification_outbox\".\"delivery_status\" = 'pending'"
+ }
+ },
+ "foreignKeys": {
+ "notification_outbox_monitor_id_monitor_id_fk": {
+ "name": "notification_outbox_monitor_id_monitor_id_fk",
+ "tableFrom": "notification_outbox",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_outbox_workspace_id_workspace_id_fk": {
+ "name": "notification_outbox_workspace_id_workspace_id_fk",
+ "tableFrom": "notification_outbox",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "notification_outbox_notification_id_notification_id_fk": {
+ "name": "notification_outbox_notification_id_notification_id_fk",
+ "tableFrom": "notification_outbox",
+ "tableTo": "notification",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_outbox_incident_id_monitor_incident_id_fk": {
+ "name": "notification_outbox_incident_id_monitor_incident_id_fk",
+ "tableFrom": "notification_outbox",
+ "tableTo": "monitor_incident",
+ "columnsFrom": [
+ "incident_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_status": {
+ "name": "monitor_status",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "region": {
+ "name": "region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_status_idx": {
+ "name": "monitor_status_idx",
+ "columns": [
+ "monitor_id",
+ "region"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_status_monitor_id_monitor_id_fk": {
+ "name": "monitor_status_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_status",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "monitor_status_monitor_id_region_pk": {
+ "columns": [
+ "monitor_id",
+ "region"
+ ],
+ "name": "monitor_status_monitor_id_region_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "invitation": {
+ "name": "invitation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "invitation_workspace_id_idx": {
+ "name": "invitation_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_incident": {
+ "name": "monitor_incident",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'triage'"
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "acknowledged_at": {
+ "name": "acknowledged_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "acknowledged_by": {
+ "name": "acknowledged_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_at": {
+ "name": "resolved_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_by": {
+ "name": "resolved_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "incident_screenshot_url": {
+ "name": "incident_screenshot_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "recovery_screenshot_url": {
+ "name": "recovery_screenshot_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "auto_resolved": {
+ "name": "auto_resolved",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_incident_workspace_id_started_at_idx": {
+ "name": "monitor_incident_workspace_id_started_at_idx",
+ "columns": [
+ "workspace_id",
+ "started_at"
+ ],
+ "isUnique": false
+ },
+ "monitor_incident_open_idx": {
+ "name": "monitor_incident_open_idx",
+ "columns": [
+ "monitor_id"
+ ],
+ "isUnique": true,
+ "where": "\"monitor_incident\".\"resolved_at\" IS NULL"
+ },
+ "monitor_incident_monitor_id_started_at_unique": {
+ "name": "monitor_incident_monitor_id_started_at_unique",
+ "columns": [
+ "monitor_id",
+ "started_at"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "monitor_incident_monitor_id_monitor_id_fk": {
+ "name": "monitor_incident_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_incident",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set default",
+ "onUpdate": "no action"
+ },
+ "monitor_incident_workspace_id_workspace_id_fk": {
+ "name": "monitor_incident_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor_incident",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "monitor_incident_acknowledged_by_user_id_fk": {
+ "name": "monitor_incident_acknowledged_by_user_id_fk",
+ "tableFrom": "monitor_incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "acknowledged_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "monitor_incident_resolved_by_user_id_fk": {
+ "name": "monitor_incident_resolved_by_user_id_fk",
+ "tableFrom": "monitor_incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "resolved_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_tag": {
+ "name": "monitor_tag",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_tag_workspace_id_idx": {
+ "name": "monitor_tag_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_tag_workspace_id_workspace_id_fk": {
+ "name": "monitor_tag_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor_tag",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_tag_to_monitor": {
+ "name": "monitor_tag_to_monitor",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_tag_id": {
+ "name": "monitor_tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_tag_to_monitor_monitor_tag_id_idx": {
+ "name": "monitor_tag_to_monitor_monitor_tag_id_idx",
+ "columns": [
+ "monitor_tag_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_tag_to_monitor_monitor_id_monitor_id_fk": {
+ "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_tag_to_monitor",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": {
+ "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk",
+ "tableFrom": "monitor_tag_to_monitor",
+ "tableTo": "monitor_tag",
+ "columnsFrom": [
+ "monitor_tag_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": {
+ "columns": [
+ "monitor_id",
+ "monitor_tag_id"
+ ],
+ "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "application": {
+ "name": "application",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dsn": {
+ "name": "dsn",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "application_dsn_unique": {
+ "name": "application_dsn_unique",
+ "columns": [
+ "dsn"
+ ],
+ "isUnique": true
+ },
+ "application_workspace_id_idx": {
+ "name": "application_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "application_workspace_id_workspace_id_fk": {
+ "name": "application_workspace_id_workspace_id_fk",
+ "tableFrom": "application",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "maintenance": {
+ "name": "maintenance",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from": {
+ "name": "from",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "to": {
+ "name": "to",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "maintenance_page_id_idx": {
+ "name": "maintenance_page_id_idx",
+ "columns": [
+ "page_id"
+ ],
+ "isUnique": false
+ },
+ "maintenance_workspace_id_idx": {
+ "name": "maintenance_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "maintenance_workspace_id_workspace_id_fk": {
+ "name": "maintenance_workspace_id_workspace_id_fk",
+ "tableFrom": "maintenance",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "maintenance_page_id_page_id_fk": {
+ "name": "maintenance_page_id_page_id_fk",
+ "tableFrom": "maintenance",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "maintenance_created_by_user_id_fk": {
+ "name": "maintenance_created_by_user_id_fk",
+ "tableFrom": "maintenance",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "maintenance_updated_by_user_id_fk": {
+ "name": "maintenance_updated_by_user_id_fk",
+ "tableFrom": "maintenance",
+ "tableTo": "user",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "maintenance_update": {
+ "name": "maintenance_update",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "date": {
+ "name": "date",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "maintenance_id": {
+ "name": "maintenance_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "maintenance_update_maintenance_id_idx": {
+ "name": "maintenance_update_maintenance_id_idx",
+ "columns": [
+ "maintenance_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "maintenance_update_maintenance_id_maintenance_id_fk": {
+ "name": "maintenance_update_maintenance_id_maintenance_id_fk",
+ "tableFrom": "maintenance_update",
+ "tableTo": "maintenance",
+ "columnsFrom": [
+ "maintenance_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "maintenance_update_created_by_user_id_fk": {
+ "name": "maintenance_update_created_by_user_id_fk",
+ "tableFrom": "maintenance_update",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "maintenance_update_updated_by_user_id_fk": {
+ "name": "maintenance_update_updated_by_user_id_fk",
+ "tableFrom": "maintenance_update",
+ "tableTo": "user",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "check": {
+ "name": "check",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "regions": {
+ "name": "regions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "url": {
+ "name": "url",
+ "type": "text(4096)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'GET'"
+ },
+ "count_requests": {
+ "name": "count_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 1
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "check_workspace_id_idx": {
+ "name": "check_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "check_workspace_id_workspace_id_fk": {
+ "name": "check_workspace_id_workspace_id_fk",
+ "tableFrom": "check",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_run": {
+ "name": "monitor_run",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "runned_at": {
+ "name": "runned_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_run_workspace_id_created_at_idx": {
+ "name": "monitor_run_workspace_id_created_at_idx",
+ "columns": [
+ "workspace_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "monitor_run_monitor_id_idx": {
+ "name": "monitor_run_monitor_id_idx",
+ "columns": [
+ "monitor_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_run_workspace_id_workspace_id_fk": {
+ "name": "monitor_run_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor_run",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "monitor_run_monitor_id_monitor_id_fk": {
+ "name": "monitor_run_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_run",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "private_location_monitor_status": {
+ "name": "private_location_monitor_status",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "private_location_id": {
+ "name": "private_location_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "private_location_monitor_status_pl_id_idx": {
+ "name": "private_location_monitor_status_pl_id_idx",
+ "columns": [
+ "private_location_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "private_location_monitor_status_monitor_id_monitor_id_fk": {
+ "name": "private_location_monitor_status_monitor_id_monitor_id_fk",
+ "tableFrom": "private_location_monitor_status",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "private_location_monitor_status_private_location_id_private_location_id_fk": {
+ "name": "private_location_monitor_status_private_location_id_private_location_id_fk",
+ "tableFrom": "private_location_monitor_status",
+ "tableTo": "private_location",
+ "columnsFrom": [
+ "private_location_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "private_location_monitor_status_monitor_id_private_location_id_pk": {
+ "columns": [
+ "monitor_id",
+ "private_location_id"
+ ],
+ "name": "private_location_monitor_status_monitor_id_private_location_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "private_location": {
+ "name": "private_location",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'error'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "private_location_workspace_id_idx": {
+ "name": "private_location_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "private_location_workspace_id_workspace_id_fk": {
+ "name": "private_location_workspace_id_workspace_id_fk",
+ "tableFrom": "private_location",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "private_location_to_monitor": {
+ "name": "private_location_to_monitor",
+ "columns": {
+ "private_location_id": {
+ "name": "private_location_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "private_location_to_monitor_private_location_id_idx": {
+ "name": "private_location_to_monitor_private_location_id_idx",
+ "columns": [
+ "private_location_id"
+ ],
+ "isUnique": false
+ },
+ "private_location_to_monitor_monitor_id_idx": {
+ "name": "private_location_to_monitor_monitor_id_idx",
+ "columns": [
+ "monitor_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "private_location_to_monitor_private_location_id_private_location_id_fk": {
+ "name": "private_location_to_monitor_private_location_id_private_location_id_fk",
+ "tableFrom": "private_location_to_monitor",
+ "tableTo": "private_location",
+ "columnsFrom": [
+ "private_location_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "private_location_to_monitor_monitor_id_monitor_id_fk": {
+ "name": "private_location_to_monitor_monitor_id_monitor_id_fk",
+ "tableFrom": "private_location_to_monitor",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_group": {
+ "name": "monitor_group",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_group_workspace_id_idx": {
+ "name": "monitor_group_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ },
+ "monitor_group_page_id_idx": {
+ "name": "monitor_group_page_id_idx",
+ "columns": [
+ "page_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_group_workspace_id_workspace_id_fk": {
+ "name": "monitor_group_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor_group",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "monitor_group_page_id_page_id_fk": {
+ "name": "monitor_group_page_id_page_id_fk",
+ "tableFrom": "monitor_group",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "viewer": {
+ "name": "viewer",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "viewer_email_unique": {
+ "name": "viewer_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "viewer_accounts": {
+ "name": "viewer_accounts",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "viewer_accounts_user_id_viewer_id_fk": {
+ "name": "viewer_accounts_user_id_viewer_id_fk",
+ "tableFrom": "viewer_accounts",
+ "tableTo": "viewer",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "viewer_accounts_provider_providerAccountId_pk": {
+ "columns": [
+ "provider",
+ "providerAccountId"
+ ],
+ "name": "viewer_accounts_provider_providerAccountId_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "viewer_session": {
+ "name": "viewer_session",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "viewer_session_user_id_viewer_id_fk": {
+ "name": "viewer_session_user_id_viewer_id_fk",
+ "tableFrom": "viewer_session",
+ "tableTo": "viewer",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "api_key": {
+ "name": "api_key",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "prefix": {
+ "name": "prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "hashed_token": {
+ "name": "hashed_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_by_id": {
+ "name": "created_by_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[\"write\"]'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "api_key_prefix_unique": {
+ "name": "api_key_prefix_unique",
+ "columns": [
+ "prefix"
+ ],
+ "isUnique": true
+ },
+ "api_key_hashed_token_unique": {
+ "name": "api_key_hashed_token_unique",
+ "columns": [
+ "hashed_token"
+ ],
+ "isUnique": true
+ },
+ "api_key_prefix_idx": {
+ "name": "api_key_prefix_idx",
+ "columns": [
+ "prefix"
+ ],
+ "isUnique": false
+ },
+ "api_key_workspace_id_idx": {
+ "name": "api_key_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "api_key_workspace_id_workspace_id_fk": {
+ "name": "api_key_workspace_id_workspace_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_created_by_id_user_id_fk": {
+ "name": "api_key_created_by_id_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "maintenance_to_page_component": {
+ "name": "maintenance_to_page_component",
+ "columns": {
+ "maintenance_id": {
+ "name": "maintenance_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_component_id": {
+ "name": "page_component_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "maintenance_to_page_component_page_component_id_idx": {
+ "name": "maintenance_to_page_component_page_component_id_idx",
+ "columns": [
+ "page_component_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "maintenance_to_page_component_maintenance_id_maintenance_id_fk": {
+ "name": "maintenance_to_page_component_maintenance_id_maintenance_id_fk",
+ "tableFrom": "maintenance_to_page_component",
+ "tableTo": "maintenance",
+ "columnsFrom": [
+ "maintenance_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "maintenance_to_page_component_page_component_id_page_component_id_fk": {
+ "name": "maintenance_to_page_component_page_component_id_page_component_id_fk",
+ "tableFrom": "maintenance_to_page_component",
+ "tableTo": "page_component",
+ "columnsFrom": [
+ "page_component_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "maintenance_to_page_component_maintenance_id_page_component_id_pk": {
+ "columns": [
+ "maintenance_id",
+ "page_component_id"
+ ],
+ "name": "maintenance_to_page_component_maintenance_id_page_component_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "page_component": {
+ "name": "page_component",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'monitor'"
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "group_order": {
+ "name": "group_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "page_component_workspace_id_idx": {
+ "name": "page_component_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ },
+ "page_component_page_id_monitor_id_unique": {
+ "name": "page_component_page_id_monitor_id_unique",
+ "columns": [
+ "page_id",
+ "monitor_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "page_component_workspace_id_workspace_id_fk": {
+ "name": "page_component_workspace_id_workspace_id_fk",
+ "tableFrom": "page_component",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "page_component_page_id_page_id_fk": {
+ "name": "page_component_page_id_page_id_fk",
+ "tableFrom": "page_component",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "page_component_monitor_id_monitor_id_fk": {
+ "name": "page_component_monitor_id_monitor_id_fk",
+ "tableFrom": "page_component",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "page_component_group_id_page_component_groups_id_fk": {
+ "name": "page_component_group_id_page_component_groups_id_fk",
+ "tableFrom": "page_component",
+ "tableTo": "page_component_groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {
+ "page_component_type_check": {
+ "name": "page_component_type_check",
+ "value": "\"page_component\".\"type\" = 'monitor' AND \"page_component\".\"monitor_id\" IS NOT NULL OR \"page_component\".\"type\" = 'static' AND \"page_component\".\"monitor_id\" IS NULL"
+ }
+ }
+ },
+ "status_report_update_to_page_component": {
+ "name": "status_report_update_to_page_component",
+ "columns": {
+ "status_report_update_id": {
+ "name": "status_report_update_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_component_id": {
+ "name": "page_component_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "impact": {
+ "name": "impact",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "status_report_update_to_page_component_page_component_id_idx": {
+ "name": "status_report_update_to_page_component_page_component_id_idx",
+ "columns": [
+ "page_component_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "status_report_update_to_page_component_status_report_update_id_status_report_update_id_fk": {
+ "name": "status_report_update_to_page_component_status_report_update_id_status_report_update_id_fk",
+ "tableFrom": "status_report_update_to_page_component",
+ "tableTo": "status_report_update",
+ "columnsFrom": [
+ "status_report_update_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "status_report_update_to_page_component_page_component_id_page_component_id_fk": {
+ "name": "status_report_update_to_page_component_page_component_id_page_component_id_fk",
+ "tableFrom": "status_report_update_to_page_component",
+ "tableTo": "page_component",
+ "columnsFrom": [
+ "page_component_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "status_report_update_to_page_component_status_report_update_id_page_component_id_pk": {
+ "columns": [
+ "status_report_update_id",
+ "page_component_id"
+ ],
+ "name": "status_report_update_to_page_component_status_report_update_id_page_component_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "status_report_to_page_component": {
+ "name": "status_report_to_page_component",
+ "columns": {
+ "status_report_id": {
+ "name": "status_report_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_component_id": {
+ "name": "page_component_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "status_report_to_page_component_page_component_id_idx": {
+ "name": "status_report_to_page_component_page_component_id_idx",
+ "columns": [
+ "page_component_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "status_report_to_page_component_status_report_id_status_report_id_fk": {
+ "name": "status_report_to_page_component_status_report_id_status_report_id_fk",
+ "tableFrom": "status_report_to_page_component",
+ "tableTo": "status_report",
+ "columnsFrom": [
+ "status_report_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "status_report_to_page_component_page_component_id_page_component_id_fk": {
+ "name": "status_report_to_page_component_page_component_id_page_component_id_fk",
+ "tableFrom": "status_report_to_page_component",
+ "tableTo": "page_component",
+ "columnsFrom": [
+ "page_component_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "status_report_to_page_component_status_report_id_page_component_id_pk": {
+ "columns": [
+ "status_report_id",
+ "page_component_id"
+ ],
+ "name": "status_report_to_page_component_status_report_id_page_component_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "page_component_groups": {
+ "name": "page_component_groups",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "default_open": {
+ "name": "default_open",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "page_component_groups_page_id_idx": {
+ "name": "page_component_groups_page_id_idx",
+ "columns": [
+ "page_id"
+ ],
+ "isUnique": false
+ },
+ "page_component_groups_workspace_id_idx": {
+ "name": "page_component_groups_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "page_component_groups_workspace_id_workspace_id_fk": {
+ "name": "page_component_groups_workspace_id_workspace_id_fk",
+ "tableFrom": "page_component_groups",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "page_component_groups_page_id_page_id_fk": {
+ "name": "page_component_groups_page_id_page_id_fk",
+ "tableFrom": "page_component_groups",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "feedback": {
+ "name": "feedback",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "blocker": {
+ "name": "blocker",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "feedback_workspace_id_idx": {
+ "name": "feedback_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "feedback_workspace_id_workspace_id_fk": {
+ "name": "feedback_workspace_id_workspace_id_fk",
+ "tableFrom": "feedback",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "feedback_user_id_user_id_fk": {
+ "name": "feedback_user_id_user_id_fk",
+ "tableFrom": "feedback",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "audit_log": {
+ "name": "audit_log",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "actor_type": {
+ "name": "actor_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "before": {
+ "name": "before",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "after": {
+ "name": "after",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "changed_fields": {
+ "name": "changed_fields",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "audit_log_workspace_created_idx": {
+ "name": "audit_log_workspace_created_idx",
+ "columns": [
+ "workspace_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "audit_log_entity_idx": {
+ "name": "audit_log_entity_idx",
+ "columns": [
+ "workspace_id",
+ "entity_type",
+ "entity_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "external_service": {
+ "name": "external_service",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "aliases": {
+ "name": "aliases",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(json_array())"
+ },
+ "name": {
+ "name": "name",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status_page_url": {
+ "name": "status_page_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "industry": {
+ "name": "industry",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "api_config": {
+ "name": "api_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "external_service_slug_unique": {
+ "name": "external_service_slug_unique",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ },
+ "external_service_deleted_at_idx": {
+ "name": "external_service_deleted_at_idx",
+ "columns": [
+ "deleted_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "external_service_component": {
+ "name": "external_service_component",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_service_id": {
+ "name": "external_service_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "upstream_component_id": {
+ "name": "upstream_component_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "aliases": {
+ "name": "aliases",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(json_array())"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "group_name": {
+ "name": "group_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "indicator": {
+ "name": "indicator",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "first_seen_at": {
+ "name": "first_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "external_service_component_unique_idx": {
+ "name": "external_service_component_unique_idx",
+ "columns": [
+ "external_service_id",
+ "upstream_component_id"
+ ],
+ "isUnique": true
+ },
+ "external_service_component_slug_unique_idx": {
+ "name": "external_service_component_slug_unique_idx",
+ "columns": [
+ "external_service_id",
+ "slug"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "external_service_component_external_service_id_external_service_id_fk": {
+ "name": "external_service_component_external_service_id_external_service_id_fk",
+ "tableFrom": "external_service_component",
+ "tableTo": "external_service",
+ "columnsFrom": [
+ "external_service_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "external_service_incident": {
+ "name": "external_service_incident",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_service_id": {
+ "name": "external_service_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_incident_id": {
+ "name": "provider_incident_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "impact": {
+ "name": "impact",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "shortlink": {
+ "name": "shortlink",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resolved_at": {
+ "name": "resolved_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "affected_component_ids": {
+ "name": "affected_component_ids",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "raw_payload": {
+ "name": "raw_payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "raw_payload_purged_at": {
+ "name": "raw_payload_purged_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "first_seen_at": {
+ "name": "first_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "external_service_incident_unique_idx": {
+ "name": "external_service_incident_unique_idx",
+ "columns": [
+ "external_service_id",
+ "provider_incident_id"
+ ],
+ "isUnique": true
+ },
+ "external_service_incident_started_at_idx": {
+ "name": "external_service_incident_started_at_idx",
+ "columns": [
+ "external_service_id",
+ "started_at"
+ ],
+ "isUnique": false
+ },
+ "external_service_incident_resolved_at_idx": {
+ "name": "external_service_incident_resolved_at_idx",
+ "columns": [
+ "resolved_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "external_service_incident_external_service_id_external_service_id_fk": {
+ "name": "external_service_incident_external_service_id_external_service_id_fk",
+ "tableFrom": "external_service_incident",
+ "tableTo": "external_service",
+ "columnsFrom": [
+ "external_service_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "external_service_report": {
+ "name": "external_service_report",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_service_id": {
+ "name": "external_service_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_service_component_id": {
+ "name": "external_service_component_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reporter_hash": {
+ "name": "reporter_hash",
+ "type": "text(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "country": {
+ "name": "country",
+ "type": "text(2)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "external_service_report_service_idx": {
+ "name": "external_service_report_service_idx",
+ "columns": [
+ "external_service_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "external_service_report_component_idx": {
+ "name": "external_service_report_component_idx",
+ "columns": [
+ "external_service_component_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "external_service_report_external_service_id_external_service_id_fk": {
+ "name": "external_service_report_external_service_id_external_service_id_fk",
+ "tableFrom": "external_service_report",
+ "tableTo": "external_service",
+ "columnsFrom": [
+ "external_service_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "external_service_report_external_service_component_id_external_service_component_id_fk": {
+ "name": "external_service_report_external_service_component_id_external_service_component_id_fk",
+ "tableFrom": "external_service_report",
+ "tableTo": "external_service_component",
+ "columnsFrom": [
+ "external_service_component_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "chat_session": {
+ "name": "chat_session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "messages": {
+ "name": "messages",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "chat_session_workspace_user_updated_idx": {
+ "name": "chat_session_workspace_user_updated_idx",
+ "columns": [
+ "workspace_id",
+ "user_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chat_session_workspace_id_workspace_id_fk": {
+ "name": "chat_session_workspace_id_workspace_id_fk",
+ "tableFrom": "chat_session",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_session_user_id_user_id_fk": {
+ "name": "chat_session_user_id_user_id_fk",
+ "tableFrom": "chat_session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "frozen_monitor_uptime": {
+ "name": "frozen_monitor_uptime",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "month": {
+ "name": "month",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "days": {
+ "name": "days",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "frozen_monitor_uptime_workspace_id_idx": {
+ "name": "frozen_monitor_uptime_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ },
+ "frozen_monitor_uptime_monitor_id_month_unique": {
+ "name": "frozen_monitor_uptime_monitor_id_month_unique",
+ "columns": [
+ "monitor_id",
+ "month"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "frozen_monitor_uptime_workspace_id_workspace_id_fk": {
+ "name": "frozen_monitor_uptime_workspace_id_workspace_id_fk",
+ "tableFrom": "frozen_monitor_uptime",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "frozen_monitor_uptime_monitor_id_monitor_id_fk": {
+ "name": "frozen_monitor_uptime_monitor_id_monitor_id_fk",
+ "tableFrom": "frozen_monitor_uptime",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_transition": {
+ "name": "monitor_transition",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "region": {
+ "name": "region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from_status": {
+ "name": "from_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "to_status": {
+ "name": "to_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "quorum_count": {
+ "name": "quorum_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "region_count": {
+ "name": "region_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "transitioned": {
+ "name": "transitioned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "outbox_rows": {
+ "name": "outbox_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "monitor_transition_monitor_id_cron_timestamp_idx": {
+ "name": "monitor_transition_monitor_id_cron_timestamp_idx",
+ "columns": [
+ "monitor_id",
+ "cron_timestamp"
+ ],
+ "isUnique": false
+ },
+ "monitor_transition_created_at_idx": {
+ "name": "monitor_transition_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_transition_monitor_id_monitor_id_fk": {
+ "name": "monitor_transition_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_transition",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "oauth_client": {
+ "name": "oauth_client",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'MCP Client'"
+ },
+ "redirect_uris": {
+ "name": "redirect_uris",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "oauth_client_client_id_unique": {
+ "name": "oauth_client_client_id_unique",
+ "columns": [
+ "client_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "oauth_session": {
+ "name": "oauth_session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resource": {
+ "name": "resource",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "code_challenge": {
+ "name": "code_challenge",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "code_challenge_method": {
+ "name": "code_challenge_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'S256'"
+ },
+ "decided_at": {
+ "name": "decided_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "oauth_session_client_id_idx": {
+ "name": "oauth_session_client_id_idx",
+ "columns": [
+ "client_id"
+ ],
+ "isUnique": false
+ },
+ "oauth_session_expires_at_idx": {
+ "name": "oauth_session_expires_at_idx",
+ "columns": [
+ "expires_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "oauth_session_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_session_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_session",
+ "tableTo": "oauth_client",
+ "columnsFrom": [
+ "client_id"
+ ],
+ "columnsTo": [
+ "client_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "oauth_authorization_code": {
+ "name": "oauth_authorization_code",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "hash": {
+ "name": "hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "code_challenge": {
+ "name": "code_challenge",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "consumed_at": {
+ "name": "consumed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "grant_id": {
+ "name": "grant_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "oauth_authorization_code_hash_unique": {
+ "name": "oauth_authorization_code_hash_unique",
+ "columns": [
+ "hash"
+ ],
+ "isUnique": true
+ },
+ "oauth_authorization_code_expires_at_idx": {
+ "name": "oauth_authorization_code_expires_at_idx",
+ "columns": [
+ "expires_at"
+ ],
+ "isUnique": false
+ },
+ "oauth_authorization_code_client_id_idx": {
+ "name": "oauth_authorization_code_client_id_idx",
+ "columns": [
+ "client_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "oauth_authorization_code_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_authorization_code_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_authorization_code",
+ "tableTo": "oauth_client",
+ "columnsFrom": [
+ "client_id"
+ ],
+ "columnsTo": [
+ "client_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_authorization_code_user_id_user_id_fk": {
+ "name": "oauth_authorization_code_user_id_user_id_fk",
+ "tableFrom": "oauth_authorization_code",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_authorization_code_workspace_id_workspace_id_fk": {
+ "name": "oauth_authorization_code_workspace_id_workspace_id_fk",
+ "tableFrom": "oauth_authorization_code",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_authorization_code_grant_id_oauth_grant_id_fk": {
+ "name": "oauth_authorization_code_grant_id_oauth_grant_id_fk",
+ "tableFrom": "oauth_authorization_code",
+ "tableTo": "oauth_grant",
+ "columnsFrom": [
+ "grant_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "oauth_grant": {
+ "name": "oauth_grant",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "access_token_hash": {
+ "name": "access_token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token_hash": {
+ "name": "refresh_token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "previous_refresh_token_hash": {
+ "name": "previous_refresh_token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rotated_at": {
+ "name": "rotated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "oauth_grant_access_token_hash_unique": {
+ "name": "oauth_grant_access_token_hash_unique",
+ "columns": [
+ "access_token_hash"
+ ],
+ "isUnique": true
+ },
+ "oauth_grant_refresh_token_hash_unique": {
+ "name": "oauth_grant_refresh_token_hash_unique",
+ "columns": [
+ "refresh_token_hash"
+ ],
+ "isUnique": true
+ },
+ "oauth_grant_workspace_id_idx": {
+ "name": "oauth_grant_workspace_id_idx",
+ "columns": [
+ "workspace_id"
+ ],
+ "isUnique": false
+ },
+ "oauth_grant_client_id_user_id_idx": {
+ "name": "oauth_grant_client_id_user_id_idx",
+ "columns": [
+ "client_id",
+ "user_id"
+ ],
+ "isUnique": false
+ },
+ "oauth_grant_user_id_idx": {
+ "name": "oauth_grant_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ },
+ "oauth_grant_previous_refresh_token_hash_idx": {
+ "name": "oauth_grant_previous_refresh_token_hash_idx",
+ "columns": [
+ "previous_refresh_token_hash"
+ ],
+ "isUnique": false
+ },
+ "oauth_grant_refresh_token_expires_at_idx": {
+ "name": "oauth_grant_refresh_token_expires_at_idx",
+ "columns": [
+ "refresh_token_expires_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "oauth_grant_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_grant_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_grant",
+ "tableTo": "oauth_client",
+ "columnsFrom": [
+ "client_id"
+ ],
+ "columnsTo": [
+ "client_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_grant_user_id_user_id_fk": {
+ "name": "oauth_grant_user_id_user_id_fk",
+ "tableFrom": "oauth_grant",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_grant_workspace_id_workspace_id_fk": {
+ "name": "oauth_grant_workspace_id_workspace_id_fk",
+ "tableFrom": "oauth_grant",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "incident": {
+ "name": "incident",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "severity": {
+ "name": "severity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'open'"
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "commander_id": {
+ "name": "commander_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "declared_by": {
+ "name": "declared_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "declared_at": {
+ "name": "declared_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mitigated_at": {
+ "name": "mitigated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_at": {
+ "name": "resolved_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_by": {
+ "name": "resolved_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "closed_at": {
+ "name": "closed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status_report_id": {
+ "name": "status_report_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "slack_channel_id": {
+ "name": "slack_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "incident_workspace_id_status_idx": {
+ "name": "incident_workspace_id_status_idx",
+ "columns": [
+ "workspace_id",
+ "status"
+ ],
+ "isUnique": false
+ },
+ "incident_workspace_id_declared_at_idx": {
+ "name": "incident_workspace_id_declared_at_idx",
+ "columns": [
+ "workspace_id",
+ "declared_at"
+ ],
+ "isUnique": false
+ },
+ "incident_slack_channel_idx": {
+ "name": "incident_slack_channel_idx",
+ "columns": [
+ "slack_team_id",
+ "slack_channel_id"
+ ],
+ "isUnique": true,
+ "where": "\"incident\".\"slack_channel_id\" IS NOT NULL"
+ },
+ "incident_status_report_id_idx": {
+ "name": "incident_status_report_id_idx",
+ "columns": [
+ "status_report_id"
+ ],
+ "isUnique": true,
+ "where": "\"incident\".\"status_report_id\" IS NOT NULL"
+ }
+ },
+ "foreignKeys": {
+ "incident_workspace_id_workspace_id_fk": {
+ "name": "incident_workspace_id_workspace_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_commander_id_user_id_fk": {
+ "name": "incident_commander_id_user_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "commander_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_declared_by_user_id_fk": {
+ "name": "incident_declared_by_user_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "declared_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_resolved_by_user_id_fk": {
+ "name": "incident_resolved_by_user_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "resolved_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_status_report_id_status_report_id_fk": {
+ "name": "incident_status_report_id_status_report_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "status_report",
+ "columnsFrom": [
+ "status_report_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "incident_event": {
+ "name": "incident_event",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "incident_id": {
+ "name": "incident_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "incident_event_incident_id_created_at_idx": {
+ "name": "incident_event_incident_id_created_at_idx",
+ "columns": [
+ "incident_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "incident_event_incident_id_incident_id_fk": {
+ "name": "incident_event_incident_id_incident_id_fk",
+ "tableFrom": "incident_event",
+ "tableTo": "incident",
+ "columnsFrom": [
+ "incident_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "incident_event_created_by_user_id_fk": {
+ "name": "incident_event_created_by_user_id_fk",
+ "tableFrom": "incident_event",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "incident_postmortem": {
+ "name": "incident_postmortem",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "incident_id": {
+ "name": "incident_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'draft'"
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_transcript": {
+ "name": "source_transcript",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "drafted_by": {
+ "name": "drafted_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "approved_by": {
+ "name": "approved_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "incident_postmortem_incident_id_unique": {
+ "name": "incident_postmortem_incident_id_unique",
+ "columns": [
+ "incident_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "incident_postmortem_incident_id_incident_id_fk": {
+ "name": "incident_postmortem_incident_id_incident_id_fk",
+ "tableFrom": "incident_postmortem",
+ "tableTo": "incident",
+ "columnsFrom": [
+ "incident_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "incident_postmortem_created_by_user_id_fk": {
+ "name": "incident_postmortem_created_by_user_id_fk",
+ "tableFrom": "incident_postmortem",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_postmortem_updated_by_user_id_fk": {
+ "name": "incident_postmortem_updated_by_user_id_fk",
+ "tableFrom": "incident_postmortem",
+ "tableTo": "user",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_postmortem_approved_by_user_id_fk": {
+ "name": "incident_postmortem_approved_by_user_id_fk",
+ "tableFrom": "incident_postmortem",
+ "tableTo": "user",
+ "columnsFrom": [
+ "approved_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "slack_user": {
+ "name": "slack_user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slack_user_id": {
+ "name": "slack_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "slack_user_user_id_idx": {
+ "name": "slack_user_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ },
+ "slack_user_workspace_id_slack_team_id_slack_user_id_unique": {
+ "name": "slack_user_workspace_id_slack_team_id_slack_user_id_unique",
+ "columns": [
+ "workspace_id",
+ "slack_team_id",
+ "slack_user_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "slack_user_workspace_id_workspace_id_fk": {
+ "name": "slack_user_workspace_id_workspace_id_fk",
+ "tableFrom": "slack_user",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "slack_user_user_id_user_id_fk": {
+ "name": "slack_user_user_id_user_id_fk",
+ "tableFrom": "slack_user",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {
+ "page_lower_slug_idx": {
+ "columns": {
+ "LOWER(\"slug\")": {
+ "isExpression": true
+ }
+ }
+ },
+ "page_lower_custom_domain_idx": {
+ "columns": {
+ "LOWER(\"custom_domain\")": {
+ "isExpression": true
+ }
+ }
+ },
+ "idx_page_subscriber_email_page_active": {
+ "columns": {
+ "LOWER(\"email\")": {
+ "isExpression": true
+ }
+ }
+ },
+ "idx_page_subscriber_webhook_page_active": {
+ "columns": {
+ "LOWER(\"webhook_url\")": {
+ "isExpression": true
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
index c6a0a9957..8f4656a66 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -652,6 +652,13 @@
"when": 1791455221245,
"tag": "0092_yielding_banshee",
"breakpoints": true
+ },
+ {
+ "idx": 93,
+ "version": "6",
+ "when": 1792006204864,
+ "tag": "0093_wonderful_red_skull",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/schema/feedbacks/constants.ts b/packages/db/src/schema/feedbacks/constants.ts
index 12e5d0388..40034d82d 100644
--- a/packages/db/src/schema/feedbacks/constants.ts
+++ b/packages/db/src/schema/feedbacks/constants.ts
@@ -4,6 +4,7 @@ export const feedbackSource = [
"onboarding-intent",
"onboarding-source",
"onboarding-completed",
+ "upgrade-intent",
] as const;
export const feedbackType = [
diff --git a/packages/db/src/schema/users/user.ts b/packages/db/src/schema/users/user.ts
index 756131f9a..f41a50270 100644
--- a/packages/db/src/schema/users/user.ts
+++ b/packages/db/src/schema/users/user.ts
@@ -100,6 +100,7 @@ export const account = sqliteTable(
primaryKey({
columns: [account.provider, account.providerAccountId],
}),
+ index("account_user_id_idx").on(account.userId),
],
);
diff --git a/packages/db/src/schema/workspaces/workspace.ts b/packages/db/src/schema/workspaces/workspace.ts
index e2d384ce7..0ee4d6956 100644
--- a/packages/db/src/schema/workspaces/workspace.ts
+++ b/packages/db/src/schema/workspaces/workspace.ts
@@ -22,6 +22,7 @@ export const workspace = sqliteTable(
endsAt: integer("ends_at", { mode: "timestamp" }),
paidUntil: integer("paid_until", { mode: "timestamp" }),
trialEndsAt: integer("trial_ends_at", { mode: "timestamp" }),
+ trialedAt: integer("trialed_at", { mode: "timestamp" }),
limits: text("limits").default("{}").notNull(),
workosOrganizationId: text("workos_organization_id").unique(),
diff --git a/packages/emails/emails/welcome.tsx b/packages/emails/emails/welcome.tsx
index 7441ac591..b65a88107 100644
--- a/packages/emails/emails/welcome.tsx
+++ b/packages/emails/emails/welcome.tsx
@@ -3,7 +3,6 @@
import { Link, Text } from "react-email";
import { Footer } from "./_components/footer";
-import { formatDay } from "./_components/format";
import { Heading } from "./_components/heading";
import { Layout } from "./_components/layout";
import { Signature } from "./_components/signature";
@@ -36,11 +35,7 @@ const links = [
},
];
-export interface WelcomeEmailProps {
- trialEndsAt?: Date;
-}
-
-const WelcomeEmail = ({ trialEndsAt }: WelcomeEmailProps = {}) => {
+const WelcomeEmail = () => {
return (
{
Thanks for signing up. A few places worth knowing about while you get
set up.
- {trialEndsAt ? (
-
- Your workspace is on a 14-day Starter trial until{" "}
- {formatDay(trialEndsAt)}, no card needed.
-
- ) : null}
{
);
};
-WelcomeEmail.PreviewProps = {
- trialEndsAt: new Date("2026-10-07T00:00:00Z"),
-} satisfies WelcomeEmailProps;
-
export default WelcomeEmail;
diff --git a/packages/emails/src/templates.test.tsx b/packages/emails/src/templates.test.tsx
index 41d02e2e7..6ebe9f700 100644
--- a/packages/emails/src/templates.test.tsx
+++ b/packages/emails/src/templates.test.tsx
@@ -711,7 +711,7 @@ describe("every transactional template", () => {
subscription: ,
magicLink: ,
dashboardMagicLink: ,
- welcome: ,
+ welcome: ,
incidentCommander: (
),
@@ -731,15 +731,9 @@ describe("every transactional template", () => {
});
describe("WelcomeEmail", () => {
- test("mentions the trial only when one started", async () => {
- const withTrial = await render(
- ,
- { plainText: true },
- );
- expect(withTrial).toContain("14-day Starter trial until Wed 7 Oct 2026");
-
- const withoutTrial = await render( , { plainText: true });
- expect(withoutTrial).not.toContain("trial");
+ test("never mentions a trial", async () => {
+ const html = await render( , { plainText: true });
+ expect(html).not.toContain("trial");
});
});
diff --git a/packages/services/src/user/__tests__/providers.test.ts b/packages/services/src/user/__tests__/providers.test.ts
new file mode 100644
index 000000000..188f21689
--- /dev/null
+++ b/packages/services/src/user/__tests__/providers.test.ts
@@ -0,0 +1,29 @@
+import { db } from "@openstatus/db";
+import { account } from "@openstatus/db/src/schema";
+import { createUser } from "@openstatus/db/src/test/factories";
+import { expect } from "@std/expect";
+import { describe, test } from "@std/testing/bdd";
+
+import { listUserProviders } from "../index.ts";
+
+async function link(userId: number, provider: string) {
+ await db.insert(account).values({
+ userId,
+ type: "oauth",
+ provider,
+ providerAccountId: crypto.randomUUID(),
+ });
+}
+
+describe("listUserProviders", () => {
+ test("one entry per linked account, empty without any", async () => {
+ const user = await createUser();
+ expect(await listUserProviders({ input: { userId: user.id } })).toEqual([]);
+
+ await link(user.id, "github");
+ await link(user.id, "resend");
+
+ const providers = await listUserProviders({ input: { userId: user.id } });
+ expect(providers.sort()).toEqual(["github", "resend"]);
+ });
+});
diff --git a/packages/services/src/user/index.ts b/packages/services/src/user/index.ts
index 3c1bff6a7..ce9e2de66 100644
--- a/packages/services/src/user/index.ts
+++ b/packages/services/src/user/index.ts
@@ -1,3 +1,8 @@
export { getUser, type UserRecord } from "./get";
export { deleteAccount } from "./delete";
-export { DeleteAccountInput, GetUserInput } from "./schemas";
+export { listUserProviders } from "./providers";
+export {
+ DeleteAccountInput,
+ GetUserInput,
+ ListUserProvidersInput,
+} from "./schemas";
diff --git a/packages/services/src/user/providers.ts b/packages/services/src/user/providers.ts
new file mode 100644
index 000000000..5164ea073
--- /dev/null
+++ b/packages/services/src/user/providers.ts
@@ -0,0 +1,22 @@
+import { db as defaultDb, eq } from "@openstatus/db";
+import { account } from "@openstatus/db/src/schema";
+
+import type { DB } from "../context";
+import { ListUserProvidersInput } from "./schemas";
+
+/** Auth providers linked to the user, one entry per `account` row. */
+export async function listUserProviders(args: {
+ input: ListUserProvidersInput;
+ db?: DB;
+}): Promise {
+ const input = ListUserProvidersInput.parse(args.input);
+ const db = args.db ?? defaultDb;
+
+ const rows = await db
+ .select({ provider: account.provider })
+ .from(account)
+ .where(eq(account.userId, input.userId))
+ .all();
+
+ return rows.map((row) => row.provider);
+}
diff --git a/packages/services/src/user/schemas.ts b/packages/services/src/user/schemas.ts
index 0d9015847..9cbd51322 100644
--- a/packages/services/src/user/schemas.ts
+++ b/packages/services/src/user/schemas.ts
@@ -9,3 +9,6 @@ export type GetUserInput = z.infer;
// future fields (e.g., reason / confirmation token).
export const DeleteAccountInput = z.object({}).strict();
export type DeleteAccountInput = z.infer;
+
+export const ListUserProvidersInput = z.object({ userId: z.number().int() });
+export type ListUserProvidersInput = z.infer;
diff --git a/packages/services/src/workspace/__tests__/trial.test.ts b/packages/services/src/workspace/__tests__/trial.test.ts
index 614cbe47b..064dc5dbe 100644
--- a/packages/services/src/workspace/__tests__/trial.test.ts
+++ b/packages/services/src/workspace/__tests__/trial.test.ts
@@ -1,3 +1,5 @@
+import { db, eq } from "@openstatus/db";
+import { selectWorkspaceSchema, workspace } from "@openstatus/db/src/schema";
import {
addUserToWorkspace,
createTestWorkspace,
@@ -7,10 +9,12 @@ import {
import { expect } from "@std/expect";
import { describe, test } from "@std/testing/bdd";
+import { makeUserCtx } from "../../../test/helpers";
import {
- findTrialEligibleWorkspace,
getTrialDaysLeft,
+ isTrialEligibleWorkspace,
listOwnedTrialWorkspaces,
+ markWorkspaceTrialed,
} from "../index.ts";
const DAY_MS = 86_400_000;
@@ -33,17 +37,24 @@ describe("getTrialDaysLeft", () => {
});
});
-describe("findTrialEligibleWorkspace", () => {
- test("an owned free workspace never linked to Stripe", async () => {
+describe("isTrialEligibleWorkspace", () => {
+ function ctxFor(
+ ws: Parameters[0],
+ userId: number,
+ ) {
+ return makeUserCtx(selectWorkspaceSchema.parse(ws), { userId });
+ }
+
+ test("an owned free workspace with no subscription", async () => {
const { workspace: ws, user } = await createTestWorkspace({
plan: "free",
stripeId: null,
subscriptionId: null,
+ trialEndsAt: null,
});
- const found = await findTrialEligibleWorkspace({
- input: { userId: user.id },
- });
- expect(found?.id).toBe(ws.id);
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, user.id) })).toBe(
+ true,
+ );
});
test("a signup workspace with no plan column yet counts as free", async () => {
@@ -55,43 +66,88 @@ describe("findTrialEligibleWorkspace", () => {
const owner = await createUser();
await addUserToWorkspace(owner.id, ws.id, "owner");
- const found = await findTrialEligibleWorkspace({
- input: { userId: owner.id },
- });
- expect(found?.id).toBe(ws.id);
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, owner.id) })).toBe(
+ true,
+ );
});
- test("null when the workspace already has a Stripe customer", async () => {
- const { user } = await createTestWorkspace({
+ test("a Stripe customer without a subscription is still eligible", async () => {
+ const { workspace: ws, user } = await createTestWorkspace({
plan: "free",
stripeId: `cus_${crypto.randomUUID()}`,
+ subscriptionId: null,
+ trialEndsAt: null,
});
- expect(
- await findTrialEligibleWorkspace({ input: { userId: user.id } }),
- ).toBeNull();
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, user.id) })).toBe(
+ true,
+ );
+ });
+
+ test("false once a trial marker is set", async () => {
+ const { workspace: ws, user } = await createTestWorkspace({
+ plan: "free",
+ stripeId: null,
+ subscriptionId: null,
+ trialEndsAt: new Date(Date.now() + DAY_MS),
+ });
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, user.id) })).toBe(
+ false,
+ );
+ });
+
+ test("false once the workspace has trialed before", async () => {
+ const { workspace: ws, user } = await createTestWorkspace({
+ plan: "free",
+ stripeId: null,
+ subscriptionId: null,
+ trialEndsAt: null,
+ trialedAt: new Date(),
+ });
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, user.id) })).toBe(
+ false,
+ );
});
- test("null when the workspace is on a paid plan", async () => {
- const { user } = await createTestWorkspace({
+ test("false when the workspace is on a paid plan", async () => {
+ const { workspace: ws, user } = await createTestWorkspace({
plan: "team",
stripeId: null,
subscriptionId: null,
});
- expect(
- await findTrialEligibleWorkspace({ input: { userId: user.id } }),
- ).toBeNull();
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, user.id) })).toBe(
+ false,
+ );
});
- test("null for a member who is not the owner", async () => {
+ test("false for a member who is not the owner", async () => {
const { workspace: ws } = await createTestWorkspace({
plan: "free",
stripeId: null,
+ subscriptionId: null,
+ trialEndsAt: null,
});
const member = await createUser();
await addUserToWorkspace(member.id, ws.id, "member");
+ expect(await isTrialEligibleWorkspace({ ctx: ctxFor(ws, member.id) })).toBe(
+ false,
+ );
+ });
+
+ test("false for an actor without a user", async () => {
+ const { workspace: ws } = await createTestWorkspace({
+ plan: "free",
+ stripeId: null,
+ subscriptionId: null,
+ trialEndsAt: null,
+ });
expect(
- await findTrialEligibleWorkspace({ input: { userId: member.id } }),
- ).toBeNull();
+ await isTrialEligibleWorkspace({
+ ctx: {
+ workspace: selectWorkspaceSchema.parse(ws),
+ actor: { type: "system", job: "test" },
+ },
+ }),
+ ).toBe(false);
});
});
@@ -118,3 +174,35 @@ describe("listOwnedTrialWorkspaces", () => {
expect(rows[0]?.trialEndsAt).toEqual(trialEndsAt);
});
});
+
+describe("markWorkspaceTrialed", () => {
+ test("sets trialedAt once and keeps the first date", async () => {
+ const first = new Date("2026-01-01T00:00:00Z");
+ const { workspace: ws, user } = await createTestWorkspace({
+ trialedAt: null,
+ });
+ const ctx = makeUserCtx(selectWorkspaceSchema.parse(ws), {
+ userId: user.id,
+ });
+
+ await markWorkspaceTrialed({ ctx });
+ const stamped = await db
+ .select({ trialedAt: workspace.trialedAt })
+ .from(workspace)
+ .where(eq(workspace.id, ws.id))
+ .get();
+ expect(stamped?.trialedAt).toBeInstanceOf(Date);
+
+ await db
+ .update(workspace)
+ .set({ trialedAt: first })
+ .where(eq(workspace.id, ws.id));
+ await markWorkspaceTrialed({ ctx });
+ const kept = await db
+ .select({ trialedAt: workspace.trialedAt })
+ .from(workspace)
+ .where(eq(workspace.id, ws.id))
+ .get();
+ expect(kept?.trialedAt).toEqual(first);
+ });
+});
diff --git a/packages/services/src/workspace/index.ts b/packages/services/src/workspace/index.ts
index 8d4b87c6f..e4fe271f8 100644
--- a/packages/services/src/workspace/index.ts
+++ b/packages/services/src/workspace/index.ts
@@ -15,9 +15,10 @@ export {
previewWorkspaceDowngrade,
} from "./downgrade";
export {
- findTrialEligibleWorkspace,
getTrialDaysLeft,
+ isTrialEligibleWorkspace,
listOwnedTrialWorkspaces,
+ markWorkspaceTrialed,
} from "./trial";
export {
updateWorkspaceLimits,
diff --git a/packages/services/src/workspace/trial.ts b/packages/services/src/workspace/trial.ts
index a46f67a46..624a4f9d2 100644
--- a/packages/services/src/workspace/trial.ts
+++ b/packages/services/src/workspace/trial.ts
@@ -12,7 +12,8 @@ import {
workspace,
} from "@openstatus/db/src/schema";
-import type { DB } from "../context";
+import { requireScope } from "../auth";
+import type { DB, ServiceContext } from "../context";
import type { Workspace } from "../types";
import { OwnedWorkspacesInput } from "./schemas";
@@ -30,34 +31,57 @@ export function getTrialDaysLeft(
}
/**
- * The workspace a signup trial can be started on: owned by the user, on the
- * free plan, and never linked to Stripe. Runs at signup, before any
- * `ctx.workspace` is resolved.
+ * Whether the actor can start a trial on `ctx.workspace`: they own it, it is
+ * on the free plan and it never had a subscription or a trial. A Stripe
+ * customer alone (an abandoned checkout) does not disqualify.
*/
-export async function findTrialEligibleWorkspace(args: {
- input: OwnedWorkspacesInput;
- db?: DB;
-}): Promise {
- const input = OwnedWorkspacesInput.parse(args.input);
- const db = args.db ?? defaultDb;
+export async function isTrialEligibleWorkspace(args: {
+ ctx: ServiceContext;
+}): Promise {
+ const { ctx } = args;
+ const db = ctx.db ?? defaultDb;
+ const userId = "userId" in ctx.actor ? ctx.actor.userId : undefined;
+ if (!userId) return false;
const row = await db
- .select({ workspace })
+ .select({ id: workspace.id })
.from(usersToWorkspaces)
.innerJoin(workspace, eq(workspace.id, usersToWorkspaces.workspaceId))
.where(
and(
- eq(usersToWorkspaces.userId, input.userId),
+ eq(usersToWorkspaces.userId, userId),
+ eq(usersToWorkspaces.workspaceId, ctx.workspace.id),
eq(usersToWorkspaces.role, "owner"),
- isNull(workspace.stripeId),
isNull(workspace.subscriptionId),
+ isNull(workspace.trialEndsAt),
+ isNull(workspace.trialedAt),
// A fresh signup workspace has no plan column yet — that is free.
or(isNull(workspace.plan), eq(workspace.plan, "free")),
),
)
.get();
- return row ? selectWorkspaceSchema.parse(row.workspace) : null;
+ return row !== undefined;
+}
+
+/**
+ * Records that the workspace used its trial. Keeps the first date on
+ * redelivery; returns false when it was already marked.
+ */
+export async function markWorkspaceTrialed(args: {
+ ctx: ServiceContext;
+}): Promise {
+ const { ctx } = args;
+ requireScope(ctx, "write");
+ const db = ctx.db ?? defaultDb;
+
+ const rows = await db
+ .update(workspace)
+ .set({ trialedAt: new Date() })
+ .where(and(eq(workspace.id, ctx.workspace.id), isNull(workspace.trialedAt)))
+ .returning({ id: workspace.id })
+ .all();
+ return rows.length > 0;
}
/** Workspaces the user owns that are mid-trial. */
--
2.51.2