feat(cloud): enable seat based pricing (#161)

* feat: setup subscriptions

* feat: prompt user to create subscription if inactive

* feat: update subscription when increasing/decreasing workspace members

* feat: rework upgrade

* chore: remove pricing notice

* chore: add translations

* chore: update proration comment
This commit is contained in:
Henry
2025-09-02 22:36:09 +01:00
committed by GitHub
parent e3d7ace8bd
commit 06c268bed1
33 changed files with 7483 additions and 4352 deletions

View File

@@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS "subscription" (
"id" bigserial PRIMARY KEY NOT NULL,
"plan" varchar(255) NOT NULL,
"referenceId" varchar(12) NOT NULL,
"stripeCustomerId" varchar(255),
"stripeSubscriptionId" varchar(255),
"status" varchar(255) NOT NULL,
"periodStart" timestamp,
"periodEnd" timestamp,
"cancelAtPeriodEnd" boolean,
"seats" integer,
"trialStart" timestamp,
"trialEnd" timestamp,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "subscription" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_referenceId_workspace_publicId_fk" FOREIGN KEY ("referenceId") REFERENCES "public"."workspace"("publicId") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,13 @@
"when": 1755094668761,
"tag": "20250813141748_AddChecklistActivityTypes",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1756803246096,
"tag": "20250902085406_AddSubscriptions",
"breakpoints": true
}
]
}

View File

@@ -1,12 +1,12 @@
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { drizzle as drizzlePgLite } from "drizzle-orm/pglite";
import { PGlite } from "@electric-sql/pglite";
import { uuid_ossp } from "@electric-sql/pglite/contrib/uuid_ossp";
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
import { drizzle as drizzlePgLite } from "drizzle-orm/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
import { Pool } from "pg";
import * as schema from "./schema";
import { PGlite } from "@electric-sql/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
export type dbClient = NodePgDatabase<typeof schema> & {
$client: Pool;
@@ -18,7 +18,10 @@ export const createDrizzleClient = (): dbClient => {
if (!connectionString) {
console.log("POSTGRES_URL environment variable is not set, using PGLite");
const client = new PGlite({ dataDir: "./pgdata", extensions: { uuid_ossp }});
const client = new PGlite({
dataDir: "./pgdata",
extensions: { uuid_ossp },
});
const db = drizzlePgLite(client, { schema });
migrate(db, { migrationsFolder: "../../packages/db/migrations" });

View File

@@ -121,6 +121,16 @@ export const getByPublicIdWithMembers = (
},
},
},
subscriptions: {
columns: {
id: true,
plan: true,
status: true,
seats: true,
periodStart: true,
periodEnd: true,
},
},
},
where: and(
eq(workspaces.publicId, workspacePublicId),

View File

@@ -10,3 +10,4 @@ export * from "./lists";
export * from "./users";
export * from "./integrations";
export * from "./workspaces";
export * from "./subscriptions";

View File

@@ -0,0 +1,37 @@
import { relations } from "drizzle-orm";
import {
bigserial,
boolean,
integer,
pgTable,
timestamp,
varchar,
} from "drizzle-orm/pg-core";
import { workspaces } from "./workspaces";
export const subscription = pgTable("subscription", {
id: bigserial("id", { mode: "number" }).primaryKey(),
plan: varchar("plan", { length: 255 }).notNull(),
referenceId: varchar("referenceId", { length: 12 })
.notNull()
.references(() => workspaces.publicId),
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 255 }),
status: varchar("status", { length: 255 }).notNull(),
periodStart: timestamp("periodStart"),
periodEnd: timestamp("periodEnd"),
cancelAtPeriodEnd: boolean("cancelAtPeriodEnd"),
seats: integer("seats"),
trialStart: timestamp("trialStart"),
trialEnd: timestamp("trialEnd"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
}).enableRLS();
export const subscriptionsRelations = relations(subscription, ({ one }) => ({
workspace: one(workspaces, {
fields: [subscription.referenceId],
references: [workspaces.publicId],
}),
}));

View File

@@ -11,6 +11,7 @@ import {
} from "drizzle-orm/pg-core";
import { boards } from "./boards";
import { subscription } from "./subscriptions";
import { users } from "./users";
export const memberRoles = ["admin", "member", "guest"] as const;
@@ -60,6 +61,7 @@ export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
}),
members: many(workspaceMembers),
boards: many(boards),
subscriptions: many(subscription),
}));
export const workspaceMembers = pgTable("workspace_members", {