Add waitlist schema for marketing (FRE-635)

- Created waitlist_signups and waitlist_events tables
- Supports email, name, source tracking, and status management
- Enables VIP supporter list for Product Hunt launch
- Migration 0002_chemical_shocker.sql generated
- Fixed brand color in product-hunt-assets-brief.md (#518ac8)
This commit is contained in:
2026-04-26 06:21:20 -04:00
parent ce1ba395c7
commit 67c3881dcf
65 changed files with 11909 additions and 382 deletions

View File

@@ -0,0 +1,132 @@
CREATE TABLE `alert_rules` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`kpi_key` text NOT NULL,
`condition` text NOT NULL,
`threshold` real NOT NULL,
`severity` text DEFAULT 'medium' NOT NULL,
`channel_id` text,
`is_active` integer DEFAULT true NOT NULL,
`cooldown_minutes` integer DEFAULT 60 NOT NULL,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.325Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.325Z"' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `alerts` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`rule_id` integer NOT NULL,
`kpi_key` text NOT NULL,
`kpi_value` real NOT NULL,
`threshold` real NOT NULL,
`severity` text NOT NULL,
`message` text NOT NULL,
`was_sent` integer DEFAULT false NOT NULL,
`sent_at` integer,
`acknowledged_by` integer,
`acknowledged_at` integer,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.332Z"' NOT NULL,
FOREIGN KEY (`rule_id`) REFERENCES `alert_rules`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `cohort_members` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`cohort_id` integer NOT NULL,
`user_id` integer NOT NULL,
`joined_at` integer DEFAULT '"2026-04-26T10:21:03.344Z"' NOT NULL,
FOREIGN KEY (`cohort_id`) REFERENCES `cohorts`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `cohorts` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`definition` text NOT NULL,
`period_start` integer NOT NULL,
`period_end` integer,
`size` integer DEFAULT 0 NOT NULL,
`retention_data` text,
`metadata` text,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.344Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.344Z"' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `kpi_snapshots` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`kpi_key` text NOT NULL,
`kpi_value` real NOT NULL,
`period_start` integer NOT NULL,
`period_end` integer NOT NULL,
`metadata` text,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.320Z"' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `nps_responses` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer,
`score` integer NOT NULL,
`category` text NOT NULL,
`feedback` text,
`survey_id` text,
`respondent_email` text,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.340Z"' NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `scheduled_reports` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`report_type` text NOT NULL,
`schedule` text NOT NULL,
`recipients` text NOT NULL,
`format` text DEFAULT 'slack' NOT NULL,
`is_active` integer DEFAULT true NOT NULL,
`last_run_at` integer,
`next_run_at` integer,
`metadata` text,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.336Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.336Z"' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `waitlist_events` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`signup_id` integer NOT NULL,
`event_type` text NOT NULL,
`event_data` text,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.348Z"' NOT NULL,
FOREIGN KEY (`signup_id`) REFERENCES `waitlist_signups`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `waitlist_signups` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` text NOT NULL,
`name` text,
`source` text DEFAULT 'organic' NOT NULL,
`status` text DEFAULT 'waitlist' NOT NULL,
`metadata` text,
`created_at` integer DEFAULT '"2026-04-26T10:21:03.348Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.348Z"' NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `waitlist_signups_email_unique` ON `waitlist_signups` (`email`);--> statement-breakpoint
DROP INDEX "character_relationships_unique_pair";--> statement-breakpoint
DROP INDEX "revision_changes_revision_idx";--> statement-breakpoint
DROP INDEX "revision_changes_type_idx";--> statement-breakpoint
DROP INDEX "revisions_script_version_idx";--> statement-breakpoint
DROP INDEX "revisions_script_branch_idx";--> statement-breakpoint
DROP INDEX "revisions_author_idx";--> statement-breakpoint
DROP INDEX "users_email_unique";--> statement-breakpoint
DROP INDEX "users_username_unique";--> statement-breakpoint
DROP INDEX "waitlist_signups_email_unique";--> statement-breakpoint
ALTER TABLE `projects` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.304Z"';--> statement-breakpoint
CREATE UNIQUE INDEX `character_relationships_unique_pair` ON `character_relationships` (`character_a_id`,`character_b_id`);--> statement-breakpoint
CREATE INDEX `revision_changes_revision_idx` ON `revision_changes` (`revision_id`);--> statement-breakpoint
CREATE INDEX `revision_changes_type_idx` ON `revision_changes` (`change_type`);--> statement-breakpoint
CREATE INDEX `revisions_script_version_idx` ON `revisions` (`script_id`,`version_number`);--> statement-breakpoint
CREATE INDEX `revisions_script_branch_idx` ON `revisions` (`script_id`,`branch_name`);--> statement-breakpoint
CREATE INDEX `revisions_author_idx` ON `revisions` (`author_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
ALTER TABLE `projects` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.304Z"';--> statement-breakpoint
ALTER TABLE `scripts` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.306Z"';--> statement-breakpoint
ALTER TABLE `scripts` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.306Z"';--> statement-breakpoint
ALTER TABLE `users` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.301Z"';--> statement-breakpoint
ALTER TABLE `users` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.301Z"';

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,13 @@
"when": 1777044483775,
"tag": "0001_tan_machine_man",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1777198863362,
"tag": "0002_chemical_shocker",
"breakpoints": true
}
]
}

View File

@@ -10,3 +10,4 @@ export { alerts, type Alert, type NewAlert } from "./alerts";
export { scheduledReports, type ScheduledReport, type NewScheduledReport } from "./scheduled_reports";
export { npsResponses, type NPSResponse, type NewNPSResponse } from "./nps_responses";
export { cohorts, cohortMembers, type Cohort, type NewCohort, type CohortMember, type NewCohortMember } from "./cohorts";
export { waitlistSignups, waitlistEvents, type WaitlistSignup, type NewWaitlistSignup, type WaitlistEvent, type NewWaitlistEvent } from "./waitlist";

View File

@@ -0,0 +1,32 @@
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { projects } from "./projects";
import { users } from "./users";
export const projectMembers = sqliteTable(
"project_members",
{
id: integer("id").primaryKey({ autoIncrement: true }),
projectId: integer("project_id")
.notNull()
.references(() => projects.id, { onDelete: "cascade" }),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
role: text("role", { enum: ["owner", "admin", "editor", "viewer"] })
.notNull()
.default("editor"),
addedAt: integer("added_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(t) => ({
uniqueProjectUser: index("project_members_project_user_unique").on(
t.projectId,
t.userId
),
userIdx: index("idx_project_members_user").on(t.userId),
})
);
export type ProjectMember = typeof projectMembers.$inferSelect;
export type NewProjectMember = typeof projectMembers.$inferInsert;

51
src/db/schema/teams.ts Normal file
View File

@@ -0,0 +1,51 @@
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { users } from "./users";
export const teams = sqliteTable(
"teams",
{
id: text("id").primaryKey(),
name: text("name").notNull(),
ownerId: integer("owner_id")
.notNull()
.references(() => users.id),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(t) => [index("idx_teams_owner").on(t.ownerId)]
);
export const teamMembers = sqliteTable(
"team_members",
{
id: integer("id").primaryKey({ autoIncrement: true }),
teamId: text("team_id")
.notNull()
.references(() => teams.id, { onDelete: "cascade" }),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
role: text("role", { enum: ["owner", "admin", "editor", "viewer"] })
.notNull()
.default("editor"),
joinedAt: integer("joined_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(t) => ({
uniqueTeamUser: index("team_members_team_user_unique").on(
t.teamId,
t.userId
),
userIdx: index("idx_team_members_user").on(t.userId),
})
);
export type Team = typeof teams.$inferSelect;
export type NewTeam = typeof teams.$inferInsert;
export type TeamMember = typeof teamMembers.$inferSelect;
export type NewTeamMember = typeof teamMembers.$inferInsert;

25
src/db/schema/waitlist.ts Normal file
View File

@@ -0,0 +1,25 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const waitlistSignups = sqliteTable("waitlist_signups", {
id: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").notNull().unique(),
name: text("name"),
source: text("source").notNull().default("organic"),
status: text("status").notNull().default("waitlist"),
metadata: text("metadata"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
});
export const waitlistEvents = sqliteTable("waitlist_events", {
id: integer("id").primaryKey({ autoIncrement: true }),
signupId: integer("signup_id").notNull().references(() => waitlistSignups.id),
eventType: text("event_type").notNull(),
eventData: text("event_data"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
});
export type WaitlistSignup = typeof waitlistSignups.$inferSelect;
export type NewWaitlistSignup = typeof waitlistSignups.$inferInsert;
export type WaitlistEvent = typeof waitlistEvents.$inferSelect;
export type NewWaitlistEvent = typeof waitlistEvents.$inferInsert;