Initial commit: Music Hub collaboration platform

Full-stack music production collaboration tool with:
- SvelteKit frontend with Design System (CSS vars, 8 shared components)
- Hono API with auth, projects, tracks, versions, comments
- PostgreSQL + Drizzle ORM (8 tables, roles, permissions)
- S3-compatible storage with presigned upload URLs
- wavesurfer.js audio player with waveform visualization
- A/B version comparison with synchronized playback
- Timestamped comments with threading and resolve workflow
- Magic Link authentication with Resend email integration
- Background audio processing (ffmpeg transcode + waveform peaks)
- Role-based access control (Owner, Engineers, Artist, Label, Management, Viewer)
- Toast notifications, skeleton loading, responsive layout
- Docker deployment setup (API + Web + Postgres)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Robin Choice
2026-04-02 13:23:10 +02:00
commit e420ed198b
88 changed files with 7306 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/schema/index.ts',
out: './src/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});

21
packages/db/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "@music-hub/db",
"version": "0.0.1",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:seed": "bun run seed.ts"
},
"dependencies": {
"@music-hub/shared": "workspace:*",
"drizzle-orm": "^0.44",
"postgres": "^3.4"
},
"devDependencies": {
"drizzle-kit": "^0.31"
}
}

12
packages/db/src/index.ts Normal file
View File

@@ -0,0 +1,12 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema/index.js';
export function createDb(connectionString: string) {
const client = postgres(connectionString);
return drizzle(client, { schema });
}
export type Database = ReturnType<typeof createDb>;
export * from './schema/index.js';

View File

@@ -0,0 +1,108 @@
CREATE TYPE "public"."project_role" AS ENUM('owner', 'recording_engineer', 'mixing_engineer', 'mastering_engineer', 'artist', 'label', 'management', 'viewer');--> statement-breakpoint
CREATE TYPE "public"."version_status" AS ENUM('uploaded', 'processing', 'ready', 'approved', 'rejected');--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"avatar_url" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "magic_links" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" varchar(255) NOT NULL,
"token" varchar(64) NOT NULL,
"expires_at" timestamp NOT NULL,
"used_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "magic_links_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token_hash" varchar(128) NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "sessions_token_hash_unique" UNIQUE("token_hash")
);
--> statement-breakpoint
CREATE TABLE "project_members" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"project_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"role" "project_role" NOT NULL,
"can_upload" boolean DEFAULT false NOT NULL,
"can_comment" boolean DEFAULT true NOT NULL,
"can_approve" boolean DEFAULT false NOT NULL,
"invited_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "project_members_project_id_user_id_unique" UNIQUE("project_id","user_id")
);
--> statement-breakpoint
CREATE TABLE "projects" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"description" text,
"cover_image_url" text,
"created_by_id" uuid NOT NULL,
"is_archived" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tracks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"project_id" uuid NOT NULL,
"name" varchar(255) NOT NULL,
"description" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_by_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "versions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"track_id" uuid NOT NULL,
"version_number" integer NOT NULL,
"label" varchar(100),
"notes" text,
"status" "version_status" DEFAULT 'uploaded' NOT NULL,
"original_file_name" varchar(500) NOT NULL,
"mime_type" varchar(100) NOT NULL,
"file_size" bigint NOT NULL,
"duration" real,
"sample_rate" integer,
"bit_depth" integer,
"original_file_key" text NOT NULL,
"stream_file_key" text,
"waveform_data_key" text,
"created_by_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "versions_track_id_version_number_unique" UNIQUE("track_id","version_number")
);
--> statement-breakpoint
CREATE TABLE "comments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"version_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"body" text NOT NULL,
"timestamp_seconds" real,
"parent_id" uuid,
"resolved_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "project_members" ADD CONSTRAINT "project_members_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "project_members" ADD CONSTRAINT "project_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "projects" ADD CONSTRAINT "projects_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tracks" ADD CONSTRAINT "tracks_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tracks" ADD CONSTRAINT "tracks_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "versions" ADD CONSTRAINT "versions_track_id_tracks_id_fk" FOREIGN KEY ("track_id") REFERENCES "public"."tracks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "versions" ADD CONSTRAINT "versions_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comments" ADD CONSTRAINT "comments_version_id_versions_id_fk" FOREIGN KEY ("version_id") REFERENCES "public"."versions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comments" ADD CONSTRAINT "comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -0,0 +1,757 @@
{
"id": "4e5be5fd-2fae-43d2-8273-5a372d714cc5",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.magic_links": {
"name": "magic_links",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"used_at": {
"name": "used_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"magic_links_token_unique": {
"name": "magic_links_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sessions_token_hash_unique": {
"name": "sessions_token_hash_unique",
"nullsNotDistinct": false,
"columns": [
"token_hash"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.project_members": {
"name": "project_members",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"project_id": {
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "project_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"can_upload": {
"name": "can_upload",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"can_comment": {
"name": "can_comment",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"can_approve": {
"name": "can_approve",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"invited_at": {
"name": "invited_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"project_members_project_id_projects_id_fk": {
"name": "project_members_project_id_projects_id_fk",
"tableFrom": "project_members",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"project_members_user_id_users_id_fk": {
"name": "project_members_user_id_users_id_fk",
"tableFrom": "project_members",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"project_members_project_id_user_id_unique": {
"name": "project_members_project_id_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"project_id",
"user_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"cover_image_url": {
"name": "cover_image_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_by_id": {
"name": "created_by_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"is_archived": {
"name": "is_archived",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"projects_created_by_id_users_id_fk": {
"name": "projects_created_by_id_users_id_fk",
"tableFrom": "projects",
"tableTo": "users",
"columnsFrom": [
"created_by_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.tracks": {
"name": "tracks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"project_id": {
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_by_id": {
"name": "created_by_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"tracks_project_id_projects_id_fk": {
"name": "tracks_project_id_projects_id_fk",
"tableFrom": "tracks",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"tracks_created_by_id_users_id_fk": {
"name": "tracks_created_by_id_users_id_fk",
"tableFrom": "tracks",
"tableTo": "users",
"columnsFrom": [
"created_by_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.versions": {
"name": "versions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"track_id": {
"name": "track_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"version_number": {
"name": "version_number",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"label": {
"name": "label",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "version_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'uploaded'"
},
"original_file_name": {
"name": "original_file_name",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"mime_type": {
"name": "mime_type",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true
},
"file_size": {
"name": "file_size",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "real",
"primaryKey": false,
"notNull": false
},
"sample_rate": {
"name": "sample_rate",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"bit_depth": {
"name": "bit_depth",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"original_file_key": {
"name": "original_file_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stream_file_key": {
"name": "stream_file_key",
"type": "text",
"primaryKey": false,
"notNull": false
},
"waveform_data_key": {
"name": "waveform_data_key",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_by_id": {
"name": "created_by_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"versions_track_id_tracks_id_fk": {
"name": "versions_track_id_tracks_id_fk",
"tableFrom": "versions",
"tableTo": "tracks",
"columnsFrom": [
"track_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"versions_created_by_id_users_id_fk": {
"name": "versions_created_by_id_users_id_fk",
"tableFrom": "versions",
"tableTo": "users",
"columnsFrom": [
"created_by_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"versions_track_id_version_number_unique": {
"name": "versions_track_id_version_number_unique",
"nullsNotDistinct": false,
"columns": [
"track_id",
"version_number"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.comments": {
"name": "comments",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"version_id": {
"name": "version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timestamp_seconds": {
"name": "timestamp_seconds",
"type": "real",
"primaryKey": false,
"notNull": false
},
"parent_id": {
"name": "parent_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"resolved_at": {
"name": "resolved_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"comments_version_id_versions_id_fk": {
"name": "comments_version_id_versions_id_fk",
"tableFrom": "comments",
"tableTo": "versions",
"columnsFrom": [
"version_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"comments_user_id_users_id_fk": {
"name": "comments_user_id_users_id_fk",
"tableFrom": "comments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.project_role": {
"name": "project_role",
"schema": "public",
"values": [
"owner",
"recording_engineer",
"mixing_engineer",
"mastering_engineer",
"artist",
"label",
"management",
"viewer"
]
},
"public.version_status": {
"name": "version_status",
"schema": "public",
"values": [
"uploaded",
"processing",
"ready",
"approved",
"rejected"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1775122377765,
"tag": "0000_magenta_apocalypse",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,21 @@
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
import { users } from './users.js';
export const magicLinks = pgTable('magic_links', {
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email', { length: 255 }).notNull(),
token: varchar('token', { length: 64 }).notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
usedAt: timestamp('used_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const sessions = pgTable('sessions', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
tokenHash: varchar('token_hash', { length: 128 }).notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});

View File

@@ -0,0 +1,19 @@
import { pgTable, uuid, text, real, timestamp } from 'drizzle-orm/pg-core';
import { users } from './users.js';
import { versions } from './tracks.js';
export const comments = pgTable('comments', {
id: uuid('id').defaultRandom().primaryKey(),
versionId: uuid('version_id')
.references(() => versions.id, { onDelete: 'cascade' })
.notNull(),
userId: uuid('user_id')
.references(() => users.id)
.notNull(),
body: text('body').notNull(),
timestampSeconds: real('timestamp_seconds'),
parentId: uuid('parent_id'),
resolvedAt: timestamp('resolved_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

View File

@@ -0,0 +1,5 @@
export * from './users.js';
export * from './auth.js';
export * from './projects.js';
export * from './tracks.js';
export * from './comments.js';

View File

@@ -0,0 +1,54 @@
import {
pgTable,
pgEnum,
uuid,
varchar,
text,
boolean,
timestamp,
unique,
} from 'drizzle-orm/pg-core';
import { users } from './users.js';
export const projectRoleEnum = pgEnum('project_role', [
'owner',
'recording_engineer',
'mixing_engineer',
'mastering_engineer',
'artist',
'label',
'management',
'viewer',
]);
export const projects = pgTable('projects', {
id: uuid('id').defaultRandom().primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
coverImageUrl: text('cover_image_url'),
createdById: uuid('created_by_id')
.references(() => users.id)
.notNull(),
isArchived: boolean('is_archived').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const projectMembers = pgTable(
'project_members',
{
id: uuid('id').defaultRandom().primaryKey(),
projectId: uuid('project_id')
.references(() => projects.id, { onDelete: 'cascade' })
.notNull(),
userId: uuid('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
role: projectRoleEnum('role').notNull(),
canUpload: boolean('can_upload').default(false).notNull(),
canComment: boolean('can_comment').default(true).notNull(),
canApprove: boolean('can_approve').default(false).notNull(),
invitedAt: timestamp('invited_at').defaultNow().notNull(),
},
(table) => [unique().on(table.projectId, table.userId)],
);

View File

@@ -0,0 +1,68 @@
import {
pgTable,
pgEnum,
uuid,
varchar,
text,
integer,
bigint,
real,
timestamp,
unique,
} from 'drizzle-orm/pg-core';
import { users } from './users.js';
import { projects } from './projects.js';
export const versionStatusEnum = pgEnum('version_status', [
'uploaded',
'processing',
'ready',
'approved',
'rejected',
]);
export const tracks = pgTable('tracks', {
id: uuid('id').defaultRandom().primaryKey(),
projectId: uuid('project_id')
.references(() => projects.id, { onDelete: 'cascade' })
.notNull(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
sortOrder: integer('sort_order').default(0).notNull(),
createdById: uuid('created_by_id')
.references(() => users.id)
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const versions = pgTable(
'versions',
{
id: uuid('id').defaultRandom().primaryKey(),
trackId: uuid('track_id')
.references(() => tracks.id, { onDelete: 'cascade' })
.notNull(),
versionNumber: integer('version_number').notNull(),
label: varchar('label', { length: 100 }),
notes: text('notes'),
status: versionStatusEnum('status').default('uploaded').notNull(),
originalFileName: varchar('original_file_name', { length: 500 }).notNull(),
mimeType: varchar('mime_type', { length: 100 }).notNull(),
fileSize: bigint('file_size', { mode: 'number' }).notNull(),
duration: real('duration'),
sampleRate: integer('sample_rate'),
bitDepth: integer('bit_depth'),
originalFileKey: text('original_file_key').notNull(),
streamFileKey: text('stream_file_key'),
waveformDataKey: text('waveform_data_key'),
createdById: uuid('created_by_id')
.references(() => users.id)
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => [unique().on(table.trackId, table.versionNumber)],
);

View File

@@ -0,0 +1,10 @@
import { pgTable, uuid, varchar, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }).notNull(),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

13
packages/db/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist"
},
"include": ["src"]
}