-
Notifications
You must be signed in to change notification settings - Fork 0
Add organizations, billing & workspace lifecycle #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
d1bde53
3ca681c
f9751fc
d1874f8
b9517ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||
| -- Create users table | ||||||
| CREATE TABLE IF NOT EXISTS users ( | ||||||
| id TEXT PRIMARY KEY, | ||||||
| email TEXT NOT NULL UNIQUE, | ||||||
| email_verified INTEGER NOT NULL DEFAULT 0, | ||||||
| password_hash TEXT NOT NULL, | ||||||
| name TEXT NOT NULL, | ||||||
| created_at INTEGER NOT NULL DEFAULT (unixepoch()) | ||||||
| ); | ||||||
|
|
||||||
| -- Create organizations table | ||||||
| CREATE TABLE IF NOT EXISTS organizations ( | ||||||
| id TEXT PRIMARY KEY, | ||||||
| name TEXT NOT NULL, | ||||||
| plan TEXT NOT NULL DEFAULT 'free', | ||||||
| org_api_key_hash TEXT UNIQUE, | ||||||
| created_at INTEGER NOT NULL DEFAULT (unixepoch()) | ||||||
| ); | ||||||
|
|
||||||
| -- Create org_memberships table | ||||||
| CREATE TABLE IF NOT EXISTS org_memberships ( | ||||||
| user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||||||
| organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, | ||||||
| role TEXT NOT NULL DEFAULT 'member', | ||||||
| created_at INTEGER NOT NULL DEFAULT (unixepoch()), | ||||||
| PRIMARY KEY (user_id, organization_id) | ||||||
| ); | ||||||
| CREATE INDEX IF NOT EXISTS idx_org_memberships_org ON org_memberships(organization_id); | ||||||
| CREATE INDEX IF NOT EXISTS idx_org_memberships_user ON org_memberships(user_id); | ||||||
|
|
||||||
| -- Create sessions table | ||||||
| CREATE TABLE IF NOT EXISTS sessions ( | ||||||
| id TEXT PRIMARY KEY, | ||||||
| user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||||||
| active_org_id TEXT REFERENCES organizations(id), | ||||||
| expires_at INTEGER NOT NULL, | ||||||
| created_at INTEGER NOT NULL DEFAULT (unixepoch()) | ||||||
| ); | ||||||
| CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); | ||||||
| CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); | ||||||
|
|
||||||
| -- Create email_verifications table | ||||||
| CREATE TABLE IF NOT EXISTS email_verifications ( | ||||||
| id TEXT PRIMARY KEY, | ||||||
| email TEXT NOT NULL, | ||||||
| code TEXT NOT NULL, | ||||||
| user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||||||
| expires_at INTEGER NOT NULL, | ||||||
| created_at INTEGER NOT NULL DEFAULT (unixepoch()) | ||||||
| ); | ||||||
| CREATE INDEX IF NOT EXISTS idx_email_verifications_user ON email_verifications(user_id); | ||||||
|
|
||||||
| -- Add new columns to workspaces | ||||||
| ALTER TABLE workspaces ADD COLUMN organization_id TEXT REFERENCES organizations(id); | ||||||
|
||||||
| ALTER TABLE workspaces ADD COLUMN organization_id TEXT REFERENCES organizations(id); | |
| ALTER TABLE workspaces ADD COLUMN organization_id TEXT REFERENCES organizations(id) ON DELETE CASCADE; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,15 +9,106 @@ import { | |||||
| import { sql } from 'drizzle-orm'; | ||||||
| import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'; | ||||||
|
|
||||||
| // ============================================ | ||||||
| // Users | ||||||
| // ============================================ | ||||||
| export const users = sqliteTable('users', { | ||||||
| id: text('id').primaryKey(), | ||||||
| email: text('email').notNull().unique(), | ||||||
| emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false), | ||||||
| passwordHash: text('password_hash').notNull(), | ||||||
| name: text('name').notNull(), | ||||||
| createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), | ||||||
| }); | ||||||
|
|
||||||
| // ============================================ | ||||||
| // Organizations | ||||||
| // ============================================ | ||||||
| export const organizations = sqliteTable('organizations', { | ||||||
| id: text('id').primaryKey(), | ||||||
| name: text('name').notNull(), | ||||||
| plan: text('plan').notNull().default('free'), | ||||||
| orgApiKeyHash: text('org_api_key_hash').unique(), | ||||||
| createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), | ||||||
| }); | ||||||
|
|
||||||
| // ============================================ | ||||||
| // Org Memberships | ||||||
| // ============================================ | ||||||
| export const orgMemberships = sqliteTable( | ||||||
| 'org_memberships', | ||||||
| { | ||||||
| userId: text('user_id') | ||||||
| .notNull() | ||||||
| .references(() => users.id, { onDelete: 'cascade' }), | ||||||
| organizationId: text('organization_id') | ||||||
| .notNull() | ||||||
| .references(() => organizations.id, { onDelete: 'cascade' }), | ||||||
| role: text('role').notNull().default('member'), // 'owner' | 'admin' | 'member' | ||||||
| createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), | ||||||
| }, | ||||||
| (table) => [ | ||||||
| primaryKey({ columns: [table.userId, table.organizationId] }), | ||||||
| index('idx_org_memberships_org').on(table.organizationId), | ||||||
| index('idx_org_memberships_user').on(table.userId), | ||||||
| ], | ||||||
| ); | ||||||
|
|
||||||
| // ============================================ | ||||||
| // Sessions (user auth) | ||||||
| // ============================================ | ||||||
| export const sessions = sqliteTable( | ||||||
| 'sessions', | ||||||
| { | ||||||
| id: text('id').primaryKey(), | ||||||
| userId: text('user_id') | ||||||
| .notNull() | ||||||
| .references(() => users.id, { onDelete: 'cascade' }), | ||||||
| activeOrgId: text('active_org_id') | ||||||
| .references(() => organizations.id), | ||||||
| expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), | ||||||
| createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), | ||||||
| }, | ||||||
| (table) => [ | ||||||
| index('idx_sessions_user').on(table.userId), | ||||||
| index('idx_sessions_expires').on(table.expiresAt), | ||||||
| ], | ||||||
| ); | ||||||
|
|
||||||
| // ============================================ | ||||||
| // Email Verifications | ||||||
| // ============================================ | ||||||
| export const emailVerifications = sqliteTable( | ||||||
| 'email_verifications', | ||||||
| { | ||||||
| id: text('id').primaryKey(), | ||||||
| email: text('email').notNull(), | ||||||
| code: text('code').notNull(), | ||||||
| userId: text('user_id') | ||||||
| .notNull() | ||||||
| .references(() => users.id, { onDelete: 'cascade' }), | ||||||
| expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), | ||||||
| createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), | ||||||
| }, | ||||||
| (table) => [ | ||||||
| index('idx_email_verifications_user').on(table.userId), | ||||||
| ], | ||||||
| ); | ||||||
|
|
||||||
| // ============================================ | ||||||
| // Workspaces | ||||||
| // ============================================ | ||||||
| export const workspaces = sqliteTable('workspaces', { | ||||||
| id: text('id').primaryKey(), | ||||||
| organizationId: text('organization_id') | ||||||
| .notNull() | ||||||
| .references(() => organizations.id, { onDelete: 'cascade' }), | ||||||
| name: text('name').notNull().unique(), | ||||||
| apiKeyHash: text('api_key_hash').notNull().unique(), | ||||||
| systemPrompt: text('system_prompt'), | ||||||
| plan: text('plan').notNull().default('free'), | ||||||
| plan: text('plan').notNull().default('free'), // deprecated: read from org | ||||||
|
||||||
| plan: text('plan').notNull().default('free'), // deprecated: read from org | |
| plan: text('plan').notNull(), // deprecated: read from org; kept for backwards compatibility, do not write to explicitly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should git ignore this and just commit a settings.json file