Initial commit of Vellum, an event photo product for guest uploads, host moderation, and original-quality galleries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 19:36:14 -04:00
co-authored by Cursor
commit 27e2f196eb
149 changed files with 13847 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
import { relations } from "drizzle-orm";
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const session = pgTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = pgTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", {
withTimezone: true,
}),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
withTimezone: true,
}),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = pgTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));
+56
View File
@@ -0,0 +1,56 @@
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { config } from "dotenv";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as appSchema from "./schema";
import * as authSchema from "./auth-schema";
for (const path of [
resolve(process.cwd(), ".env"),
resolve(process.cwd(), "../../.env"),
]) {
if (existsSync(path)) {
config({ path, override: false });
break;
}
}
const schema = { ...appSchema, ...authSchema };
type PostgresClient = ReturnType<typeof postgres>;
type Database = ReturnType<typeof drizzle<typeof schema>>;
export type { Database };
const globalDatabase = globalThis as typeof globalThis & {
albumPostgresClient?: PostgresClient;
albumDatabase?: Database;
};
let client = globalDatabase.albumPostgresClient;
let database = globalDatabase.albumDatabase;
export function getDb() {
if (database) return database;
const url = process.env.DATABASE_URL;
if (!url) {
throw new Error("DATABASE_URL is required for database operations");
}
client = postgres(url, {
max: process.env.NODE_ENV === "production" ? 10 : 3,
idle_timeout: 20,
});
database = drizzle(client, { schema });
if (process.env.NODE_ENV !== "production") {
globalDatabase.albumPostgresClient = client;
globalDatabase.albumDatabase = database;
}
return database;
}
export async function closeDb() {
await client?.end();
client = undefined;
database = undefined;
globalDatabase.albumPostgresClient = undefined;
globalDatabase.albumDatabase = undefined;
}
+3
View File
@@ -0,0 +1,3 @@
export { closeDb, getDb, type Database } from "./db";
export * from "./schema";
export * from "./auth-schema";
+16
View File
@@ -0,0 +1,16 @@
import "dotenv/config";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
const url =
process.env.DATABASE_URL ??
"postgres://album:album@localhost:5439/album";
const client = postgres(url, { max: 1 });
const db = drizzle(client);
await migrate(db, { migrationsFolder: "./drizzle" });
await client.end();
console.log("Migrations applied successfully.");
+491
View File
@@ -0,0 +1,491 @@
import {
boolean,
index,
integer,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { relations } from "drizzle-orm";
import { user } from "./auth-schema";
const timestamps = {
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
};
export const eventStatus = pgEnum("event_status", [
"draft",
"published",
"closed",
]);
export const groupRole = pgEnum("group_role", ["owner", "member"]);
export const eventRole = pgEnum("event_role", [
"owner",
"manager",
"moderator",
"viewer",
]);
export const platformRole = pgEnum("platform_role", [
"super_admin",
"admin",
"moderator",
"viewer",
]);
export const eventCreatePolicy = pgEnum("event_create_policy", [
"open",
"invite",
"admin_only",
]);
export const entitlementSource = pgEnum("entitlement_source", [
"signup_default",
"invite",
"code",
"platform",
]);
export const inviteKind = pgEnum("invite_kind", ["email", "code"]);
export const inviteStatus = pgEnum("invite_status", [
"pending",
"accepted",
"revoked",
"expired",
]);
export const photoProcessingStatus = pgEnum("photo_processing_status", [
"uploading",
"processing",
"ready",
"failed",
]);
export const photoVisibility = pgEnum("photo_visibility", [
"pending",
"public",
"hidden",
"private",
"rejected",
]);
export const photoJobStatus = pgEnum("photo_job_status", [
"pending",
"processing",
"completed",
"failed",
]);
export const groups = pgTable(
"groups",
{
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
slug: text("slug").notNull(),
createdByUserId: text("created_by_user_id").references(() => user.id, {
onDelete: "set null",
}),
...timestamps,
},
(table) => [
uniqueIndex("groups_slug_idx").on(table.slug),
index("groups_created_by_user_id_idx").on(table.createdByUserId),
],
);
export const groupMemberships = pgTable(
"group_memberships",
{
id: uuid("id").defaultRandom().primaryKey(),
groupId: uuid("group_id")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: groupRole("role").notNull().default("member"),
...timestamps,
},
(table) => [
uniqueIndex("group_memberships_group_user_idx").on(
table.groupId,
table.userId,
),
index("group_memberships_user_idx").on(table.userId),
],
);
export const events = pgTable(
"events",
{
id: uuid("id").defaultRandom().primaryKey(),
groupId: uuid("group_id")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
slug: text("slug").notNull(),
title: text("title").notNull(),
description: text("description"),
startsAt: timestamp("starts_at", { withTimezone: true }),
endsAt: timestamp("ends_at", { withTimezone: true }),
status: eventStatus("status").notNull().default("draft"),
listed: boolean("listed").notNull().default(false),
uploadEnabled: boolean("upload_enabled").notNull().default(true),
galleryReleasedAt: timestamp("gallery_released_at", { withTimezone: true }),
...timestamps,
},
(table) => [
uniqueIndex("events_slug_idx").on(table.slug),
index("events_group_id_idx").on(table.groupId),
index("events_listed_idx").on(table.listed, table.status),
],
);
export const eventMemberships = pgTable(
"event_memberships",
{
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id")
.notNull()
.references(() => events.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: eventRole("role").notNull().default("viewer"),
...timestamps,
},
(table) => [
uniqueIndex("event_memberships_event_user_idx").on(
table.eventId,
table.userId,
),
index("event_memberships_user_idx").on(table.userId),
],
);
export const platformAdministrators = pgTable(
"platform_administrators",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: platformRole("role").notNull().default("admin"),
...timestamps,
},
(table) => [uniqueIndex("platform_administrators_user_idx").on(table.userId)],
);
export const deploymentSettings = pgTable("deployment_settings", {
id: text("id").primaryKey().default("default"),
openSignup: boolean("open_signup").notNull().default(true),
eventCreatePolicy: eventCreatePolicy("event_create_policy")
.notNull()
.default("open"),
defaultEventLimit: integer("default_event_limit").notNull().default(1),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
});
export const entitlements = pgTable(
"entitlements",
{
id: uuid("id").defaultRandom().primaryKey(),
groupId: uuid("group_id")
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
eventLimit: integer("event_limit"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
complimentary: boolean("complimentary").notNull().default(false),
source: entitlementSource("source").notNull().default("signup_default"),
grantedByUserId: text("granted_by_user_id").references(() => user.id, {
onDelete: "set null",
}),
...timestamps,
},
(table) => [index("entitlements_group_id_idx").on(table.groupId)],
);
export const invites = pgTable(
"invites",
{
id: uuid("id").defaultRandom().primaryKey(),
kind: inviteKind("kind").notNull(),
status: inviteStatus("status").notNull().default("pending"),
email: text("email"),
tokenHash: text("token_hash").notNull(),
reusable: boolean("reusable").notNull().default(false),
maxUses: integer("max_uses").notNull().default(1),
usedCount: integer("used_count").notNull().default(0),
groupId: uuid("group_id").references(() => groups.id, {
onDelete: "cascade",
}),
eventId: uuid("event_id").references(() => events.id, {
onDelete: "cascade",
}),
groupRole: groupRole("group_role"),
eventRole: eventRole("event_role"),
grantUnlimitedEvents: boolean("grant_unlimited_events")
.notNull()
.default(false),
grantEventLimit: integer("grant_event_limit"),
grantComplimentary: boolean("grant_complimentary").notNull().default(false),
createdByUserId: text("created_by_user_id").references(() => user.id, {
onDelete: "set null",
}),
acceptedByUserId: text("accepted_by_user_id").references(() => user.id, {
onDelete: "set null",
}),
expiresAt: timestamp("expires_at", { withTimezone: true }),
acceptedAt: timestamp("accepted_at", { withTimezone: true }),
...timestamps,
},
(table) => [
uniqueIndex("invites_token_hash_idx").on(table.tokenHash),
index("invites_email_idx").on(table.email),
index("invites_group_id_idx").on(table.groupId),
index("invites_event_id_idx").on(table.eventId),
],
);
export const guests = pgTable(
"guests",
{
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id")
.notNull()
.references(() => events.id, { onDelete: "cascade" }),
displayName: text("display_name"),
email: text("email"),
notifyWhenReady: boolean("notify_when_ready").notNull().default(false),
notifiedAt: timestamp("notified_at", { withTimezone: true }),
note: text("note"),
tokenHash: text("token_hash").notNull(),
...timestamps,
},
(table) => [
uniqueIndex("guests_event_token_idx").on(table.eventId, table.tokenHash),
index("guests_event_id_idx").on(table.eventId),
],
);
export const submissions = pgTable(
"submissions",
{
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id")
.notNull()
.references(() => events.id, { onDelete: "cascade" }),
guestId: uuid("guest_id")
.notNull()
.references(() => guests.id, { onDelete: "cascade" }),
...timestamps,
},
(table) => [
index("submissions_event_id_idx").on(table.eventId),
index("submissions_guest_id_idx").on(table.guestId),
],
);
export const photos = pgTable(
"photos",
{
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id")
.notNull()
.references(() => events.id, { onDelete: "cascade" }),
submissionId: uuid("submission_id")
.notNull()
.references(() => submissions.id, { onDelete: "cascade" }),
processingStatus: photoProcessingStatus("processing_status")
.notNull()
.default("uploading"),
visibility: photoVisibility("visibility").notNull().default("pending"),
originalKey: text("original_key").notNull(),
displayKey: text("display_key"),
thumbKey: text("thumb_key"),
contentType: text("content_type").notNull(),
byteSize: integer("byte_size").notNull().default(0),
width: integer("width"),
height: integer("height"),
...timestamps,
},
(table) => [
index("photos_event_id_idx").on(table.eventId),
index("photos_submission_id_idx").on(table.submissionId),
index("photos_event_visibility_idx").on(table.eventId, table.visibility),
],
);
export const photoJobs = pgTable(
"photo_jobs",
{
id: uuid("id").defaultRandom().primaryKey(),
photoId: uuid("photo_id")
.notNull()
.references(() => photos.id, { onDelete: "cascade" }),
kind: text("kind").notNull().default("transcode"),
status: photoJobStatus("status").notNull().default("pending"),
attempts: integer("attempts").notNull().default(0),
lastError: text("last_error"),
...timestamps,
},
(table) => [
index("photo_jobs_status_idx").on(table.status),
index("photo_jobs_photo_id_idx").on(table.photoId),
],
);
export const auditEvents = pgTable(
"audit_events",
{
id: uuid("id").defaultRandom().primaryKey(),
groupId: uuid("group_id").references(() => groups.id, {
onDelete: "set null",
}),
eventId: uuid("event_id").references(() => events.id, {
onDelete: "set null",
}),
actorUserId: text("actor_user_id").references(() => user.id, {
onDelete: "set null",
}),
action: text("action").notNull(),
subjectType: text("subject_type").notNull(),
subjectId: text("subject_id").notNull(),
metadata: jsonb("metadata")
.$type<Record<string, string | number | boolean | null>>()
.notNull()
.default(sql`'{}'::jsonb`),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => [
index("audit_events_group_id_idx").on(table.groupId),
index("audit_events_event_id_idx").on(table.eventId),
index("audit_events_created_at_idx").on(table.createdAt),
],
);
export const rateLimitBuckets = pgTable("rate_limit_buckets", {
key: text("key").primaryKey(),
count: integer("count").notNull(),
resetAt: timestamp("reset_at", { withTimezone: true }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
});
export const groupRelations = relations(groups, ({ many }) => ({
memberships: many(groupMemberships),
events: many(events),
entitlements: many(entitlements),
}));
export const groupMembershipRelations = relations(
groupMemberships,
({ one }) => ({
group: one(groups, {
fields: [groupMemberships.groupId],
references: [groups.id],
}),
user: one(user, {
fields: [groupMemberships.userId],
references: [user.id],
}),
}),
);
export const eventRelations = relations(events, ({ one, many }) => ({
group: one(groups, {
fields: [events.groupId],
references: [groups.id],
}),
memberships: many(eventMemberships),
photos: many(photos),
guests: many(guests),
submissions: many(submissions),
}));
export const eventMembershipRelations = relations(
eventMemberships,
({ one }) => ({
event: one(events, {
fields: [eventMemberships.eventId],
references: [events.id],
}),
user: one(user, {
fields: [eventMemberships.userId],
references: [user.id],
}),
}),
);
export const guestRelations = relations(guests, ({ one, many }) => ({
event: one(events, {
fields: [guests.eventId],
references: [events.id],
}),
submissions: many(submissions),
}));
export const submissionRelations = relations(submissions, ({ one, many }) => ({
event: one(events, {
fields: [submissions.eventId],
references: [events.id],
}),
guest: one(guests, {
fields: [submissions.guestId],
references: [guests.id],
}),
photos: many(photos),
}));
export const photoRelations = relations(photos, ({ one, many }) => ({
event: one(events, {
fields: [photos.eventId],
references: [events.id],
}),
submission: one(submissions, {
fields: [photos.submissionId],
references: [submissions.id],
}),
jobs: many(photoJobs),
}));
export const photoJobRelations = relations(photoJobs, ({ one }) => ({
photo: one(photos, {
fields: [photoJobs.photoId],
references: [photos.id],
}),
}));
export const platformAdministratorRelations = relations(
platformAdministrators,
({ one }) => ({
user: one(user, {
fields: [platformAdministrators.userId],
references: [user.id],
}),
}),
);
export const entitlementRelations = relations(entitlements, ({ one }) => ({
group: one(groups, {
fields: [entitlements.groupId],
references: [groups.id],
}),
}));
+173
View File
@@ -0,0 +1,173 @@
import "dotenv/config";
import { and, eq } from "drizzle-orm";
import {
closeDb,
deploymentSettings,
entitlements,
eventMemberships,
events,
getDb,
groupMemberships,
groups,
platformAdministrators,
user,
} from "./index";
const db = getDb();
async function userByEmail(email: string) {
const [row] = await db.select().from(user).where(eq(user.email, email)).limit(1);
return row ?? null;
}
const admin = await userByEmail("admin@example.com");
const host = await userByEmail("host@example.com");
const partner = await userByEmail("partner@example.com");
const manager = await userByEmail("manager@example.com");
if (!admin || !host || !partner || !manager) {
console.info(
"Seed users missing. Run bun run auth:seed, then bun run db:seed again.",
);
await closeDb();
process.exit(0);
}
await db
.insert(deploymentSettings)
.values({ id: "default" })
.onConflictDoNothing();
const [existingAdmin] = await db
.select()
.from(platformAdministrators)
.where(eq(platformAdministrators.userId, admin.id))
.limit(1);
if (!existingAdmin) {
await db.insert(platformAdministrators).values({
userId: admin.id,
role: "super_admin",
});
}
let [group] = await db
.select()
.from(groups)
.where(eq(groups.slug, "demo-family"))
.limit(1);
if (!group) {
[group] = await db
.insert(groups)
.values({
name: "Demo family",
slug: "demo-family",
createdByUserId: host.id,
})
.returning();
}
if (group) {
for (const member of [
{ userId: host.id, role: "owner" as const },
{ userId: partner.id, role: "owner" as const },
{ userId: manager.id, role: "member" as const },
]) {
const [existing] = await db
.select()
.from(groupMemberships)
.where(
and(
eq(groupMemberships.groupId, group.id),
eq(groupMemberships.userId, member.userId),
),
)
.limit(1);
if (!existing) {
await db.insert(groupMemberships).values({
groupId: group.id,
userId: member.userId,
role: member.role,
});
}
}
const [entitlement] = await db
.select()
.from(entitlements)
.where(eq(entitlements.groupId, group.id))
.limit(1);
if (!entitlement) {
await db.insert(entitlements).values({
groupId: group.id,
eventLimit: null,
complimentary: true,
source: "platform",
grantedByUserId: admin.id,
});
}
let [event] = await db
.select()
.from(events)
.where(eq(events.slug, "demo"))
.limit(1);
if (!event) {
[event] = await db
.insert(events)
.values({
groupId: group.id,
slug: "demo",
title: "Demo gathering",
description:
"A sample event so you can try guest uploads and host moderation.",
status: "published",
listed: true,
uploadEnabled: true,
galleryReleasedAt: new Date(),
startsAt: new Date(),
})
.returning();
console.info("Seeded published event /e/demo");
} else {
await db
.update(events)
.set({
groupId: group.id,
listed: true,
galleryReleasedAt: event.galleryReleasedAt ?? new Date(),
status: "published",
updatedAt: new Date(),
})
.where(eq(events.id, event.id));
console.info("Demo event already exists");
}
if (event) {
for (const member of [
{ userId: host.id, role: "owner" as const },
{ userId: partner.id, role: "owner" as const },
{ userId: manager.id, role: "manager" as const },
]) {
const [existing] = await db
.select()
.from(eventMemberships)
.where(
and(
eq(eventMemberships.eventId, event.id),
eq(eventMemberships.userId, member.userId),
),
)
.limit(1);
if (!existing) {
await db.insert(eventMemberships).values({
eventId: event.id,
userId: member.userId,
role: member.role,
});
}
}
}
}
console.info("Seeded demo group, two owners, a manager, and platform super-admin.");
await closeDb();