Files
classroomio/packages/db/src/schema.ts
T

1995 lines
64 KiB
TypeScript
Raw Normal View History

2025-11-01 15:49:29 +01:00
import {
bigint,
boolean,
doublePrecision,
foreignKey,
index,
integer,
json,
jsonb,
pgEnum,
pgTable,
pgView,
serial,
text,
timestamp,
unique,
uuid,
varchar
} from 'drizzle-orm/pg-core';
2026-03-09 11:18:22 +01:00
import type { AnswerData } from '@cio/question-types';
2025-11-01 15:49:29 +01:00
import { sql } from 'drizzle-orm';
export const courseType = pgEnum('COURSE_TYPE', ['SELF_PACED', 'LIVE_CLASS']);
export const locale = pgEnum('LOCALE', ['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']);
export const plan = pgEnum('PLAN', ['EARLY_ADOPTER', 'ENTERPRISE', 'BASIC']);
2026-03-09 11:18:22 +01:00
export const courseInviteEventType = pgEnum('COURSE_INVITE_EVENT_TYPE', [
'CREATED',
'REVOKED',
'PREVIEWED',
'ACCEPTED',
'EMAIL_SENT',
'EMAIL_FAILED',
'ABUSE_BLOCKED'
]);
export const organizationInviteEventType = pgEnum('ORGANIZATION_INVITE_EVENT_TYPE', [
'CREATED',
'REVOKED',
'PREVIEWED',
'ACCEPTED',
'EMAIL_SENT',
'EMAIL_FAILED',
'ABUSE_BLOCKED'
]);
2025-11-01 15:49:29 +01:00
export const user = pgTable('user', {
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').default(false).notNull(),
image: text('image'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
role: text('role'),
banned: boolean('banned').default(false),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires'),
isAnonymous: boolean('is_anonymous')
});
2026-03-09 11:18:22 +01:00
export const ssoProvider = pgTable('sso_provider', {
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
issuer: text('issuer').notNull(),
oidcConfig: text('oidc_config'),
samlConfig: text('saml_config'),
userId: uuid('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull().unique(),
organizationId: text('organization_id'),
domain: text('domain').notNull()
});
export const session = pgTable('session', {
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at')
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: uuid('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
impersonatedBy: text('impersonated_by')
});
export const account = pgTable('account', {
id: uuid('id')
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: uuid('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'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at')
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull()
});
export const verification = pgTable('verification', {
id: uuid('id')
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull()
});
2025-11-01 15:49:29 +01:00
export const analyticsLoginEvents = pgTable(
'analytics_login_events',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
userId: uuid('user_id').notNull(),
loggedInAt: timestamp('logged_in_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => [
index('idx_analytics_login_events_logged_in_at').using(
'btree',
table.loggedInAt.asc().nullsLast().op('timestamptz_ops')
),
index('idx_analytics_login_events_user_id').using('btree', table.userId.asc().nullsLast().op('uuid_ops')),
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'analytics_login_events_user_id_fkey'
}),
unique('analytics_login_events_user_id_unique').on(table.userId)
2025-11-01 15:49:29 +01:00
]
);
2026-03-09 11:18:22 +01:00
export const courseSection = pgTable(
'course_section',
2025-11-01 15:49:29 +01:00
{
id: uuid().defaultRandom().primaryKey().notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
title: varchar(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
order: bigint({ mode: 'number' }).default(sql`'0'`),
courseId: uuid('course_id')
},
(table) => [
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
2026-03-09 11:18:22 +01:00
name: 'public_course_section_course_id_fkey'
2025-11-01 15:49:29 +01:00
})
.onUpdate('cascade')
.onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
export const group = pgTable(
'group',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
name: varchar().notNull(),
description: text(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
organizationId: uuid('organization_id')
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'group_organization_id_fkey'
2026-03-09 11:18:22 +01:00
}),
index('idx_group_organization_id').on(table.organizationId)
2025-11-01 15:49:29 +01:00
]
);
export const groupAttendance = pgTable(
'group_attendance',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'group_attendance_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
courseId: uuid('course_id'),
studentId: uuid('student_id'),
isPresent: boolean('is_present').default(false),
lessonId: uuid('lesson_id').notNull()
},
(table) => [
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'group_attendance_course_id_fkey'
}),
foreignKey({
columns: [table.studentId],
foreignColumns: [groupmember.id],
name: 'group_attendance_student_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const currency = pgTable('currency', {
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'currency_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
name: varchar()
});
export const appsPollOption = pgTable(
'apps_poll_option',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'apps_poll_option_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }),
pollId: uuid('poll_id'),
label: varchar()
},
(table) => [
foreignKey({
columns: [table.pollId],
foreignColumns: [appsPoll.id],
name: 'apps_poll_option_poll_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const profile = pgTable(
'profile',
{
id: uuid().primaryKey().notNull(),
fullname: text().notNull(),
username: text().notNull(),
avatarUrl: text('avatar_url').default(
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/avatar.png'
),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
email: varchar(),
canAddCourse: boolean('can_add_course').default(true),
role: varchar(),
goal: varchar(),
source: varchar(),
metadata: json(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
telegramChatId: bigint('telegram_chat_id', { mode: 'number' }),
isEmailVerified: boolean('is_email_verified').default(false),
verifiedAt: timestamp('verified_at', { withTimezone: true, mode: 'string' }),
locale: locale().default('en'),
isRestricted: boolean('is_restricted').default(false).notNull()
},
(table) => [
foreignKey({
columns: [table.id],
foreignColumns: [user.id],
name: 'profile_id_fkey'
}),
unique('profile_username_key').on(table.username),
unique('profile_email_key').on(table.email),
index('idx_profile_id').on(table.id),
index('idx_profile_email').on(table.email)
2025-11-01 15:49:29 +01:00
]
);
export const option = pgTable(
'option',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'option_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
label: varchar().notNull(),
isCorrect: boolean('is_correct').default(false).notNull(),
2026-03-09 11:18:22 +01:00
settings: jsonb().default({}),
2025-11-01 15:49:29 +01:00
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
questionId: bigint('question_id', { mode: 'number' }).notNull(),
value: uuid().default(sql`gen_random_uuid()`),
2025-11-01 15:49:29 +01:00
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => [
foreignKey({
columns: [table.questionId],
foreignColumns: [question.id],
name: 'option_question_id_fkey'
}).onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
export const quizPlay = pgTable(
'quiz_play',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'play_quiz_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
quizId: uuid('quiz_id'),
players: json().default([]),
started: boolean().default(false),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
currentQuestionId: bigint({ mode: 'number' }).default(sql`'0'`),
showCurrentQuestionAnswer: boolean().default(false),
isLastQuestion: boolean(),
step: text().default('CONNECT_TO_PLAY'),
studentStep: text().default('PIN_SETUP'),
pin: text()
},
(table) => [
foreignKey({
columns: [table.quizId],
foreignColumns: [quiz.id],
name: 'quiz_play_quiz_id_fkey'
}),
unique('quiz_play_pin_key').on(table.pin)
2025-11-01 15:49:29 +01:00
]
);
export const organizationContacts = pgTable(
'organization_contacts',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'organization_contacts_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
email: text(),
phone: text(),
name: text(),
message: text(),
organizationId: uuid('organization_id')
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'organization_contacts_organization_id_fkey'
})
]
);
export const lessonComment = pgTable(
'lesson_comment',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'lesson_comment_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
lessonId: uuid('lesson_id'),
groupmemberId: uuid('groupmember_id'),
comment: text()
},
(table) => [
foreignKey({
columns: [table.groupmemberId],
foreignColumns: [groupmember.id],
name: 'lesson_comment_groupmember_id_fkey'
}),
foreignKey({
columns: [table.lessonId],
foreignColumns: [lesson.id],
name: 'lesson_comment_lesson_id_fkey'
}).onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
export const quiz = pgTable(
'quiz',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
title: text(),
questions: json(),
timelimit: varchar().default('10s'),
theme: varchar().default('standard'),
organizationId: uuid('organization_id').notNull()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'quiz_organization_id_fkey'
})
]
);
export const submission = pgTable(
'submission',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
reviewerId: bigint('reviewer_id', { mode: 'number' }),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
statusId: bigint('status_id', { mode: 'number' }).default(sql`'1'`),
2026-03-09 11:18:22 +01:00
gradingState: varchar('grading_state').default('queued').notNull(),
overallStatus: varchar('overall_status').default('manual_required').notNull(),
2025-11-01 15:49:29 +01:00
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
total: bigint({ mode: 'number' }).default(sql`'0'`),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
exerciseId: uuid('exercise_id').notNull(),
submittedBy: uuid('submitted_by'),
courseId: uuid('course_id'),
feedback: text()
},
(table) => [
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'submission_course_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.exerciseId],
foreignColumns: [exercise.id],
name: 'submission_exercise_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.statusId],
foreignColumns: [submissionstatus.id],
name: 'submission_status_id_fkey'
}),
foreignKey({
columns: [table.submittedBy],
foreignColumns: [groupmember.id],
name: 'submission_submitted_by_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const submissionstatus = pgTable(
'submissionstatus',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'submission_status_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
label: varchar().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => []
2025-11-01 15:49:29 +01:00
);
export const course = pgTable(
'course',
{
title: varchar().notNull(),
description: varchar().notNull(),
overview: varchar().default('Welcome to this amazing course 🚀 '),
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
groupId: uuid('group_id'),
isTemplate: boolean('is_template').default(true),
logo: text().default('').notNull(),
slug: varchar(),
2026-03-09 11:18:22 +01:00
metadata: jsonb().default({ goals: '', description: '', requirements: '' }).notNull().$type<{
requirements?: string;
description?: string;
goals?: string;
videoUrl?: string;
showDiscount?: boolean;
discount?: number;
paymentLink?: string;
reward?: {
show: boolean;
description: string;
};
instructor?: {
name: string;
role: string;
coursesNo: number;
description: string;
imgUrl: string;
};
certificate?: {
templateUrl: string;
};
reviews?: {
id: number;
hide: boolean;
name: string;
avatar_url: string;
rating: number;
created_at: number;
description: string;
}[];
lessonTabsOrder?: {
id: 1 | 2 | 3 | 4;
name: string;
}[];
grading?: boolean;
lessonDownload?: boolean;
allowNewStudent: boolean;
sectionDisplay?: Record<string, boolean>;
isContentGroupingEnabled?: boolean;
}>(),
2025-11-01 15:49:29 +01:00
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
cost: bigint({ mode: 'number' }).default(sql`'0'`),
currency: varchar().default('USD').notNull(),
bannerImage: text('banner_image'),
isPublished: boolean('is_published').default(false),
isCertificateDownloadable: boolean('is_certificate_downloadable').default(false),
certificateTheme: text('certificate_theme'),
status: text().default('ACTIVE').notNull(),
2026-03-09 11:18:22 +01:00
type: courseType().default('LIVE_CLASS')
2025-11-01 15:49:29 +01:00
},
(table) => [
foreignKey({
columns: [table.groupId],
foreignColumns: [group.id],
name: 'course_group_id_fkey'
}),
2026-03-09 11:18:22 +01:00
unique('course_slug_key').on(table.slug),
index('idx_course_group_id').on(table.groupId)
2025-11-01 15:49:29 +01:00
]
);
export const appsPoll = pgTable(
'apps_poll',
{
id: uuid().defaultRandom().primaryKey().notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
question: text(),
authorId: uuid(),
isPublic: boolean(),
status: varchar().default('draft'),
expiration: timestamp({ withTimezone: true, mode: 'string' }),
courseId: uuid()
},
(table) => [
foreignKey({
columns: [table.authorId],
foreignColumns: [groupmember.id],
name: 'apps_poll_authorId_fkey'
}),
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'apps_poll_courseId_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const videoTranscripts = pgTable('video_transcripts', {
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'video_transcripts_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
museSvid: text('muse_svid'),
transcript: text(),
downloaded: boolean().default(false),
link: text()
});
export const organizationEmaillist = pgTable(
'organization_emaillist',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'organization_emaillist_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
email: text(),
organizationId: uuid('organization_id')
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'organization_emaillist_organization_id_fkey'
})
]
);
export const lesson = pgTable(
'lesson',
{
note: varchar(),
videoUrl: varchar('video_url'),
slideUrl: varchar('slide_url'),
courseId: uuid('course_id').notNull(),
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
title: varchar().notNull(),
public: boolean().default(false),
lessonAt: timestamp('lesson_at', { withTimezone: true, mode: 'string' }).defaultNow(),
teacherId: uuid('teacher_id'),
isComplete: boolean('is_complete').default(false),
callUrl: text('call_url'),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
order: bigint({ mode: 'number' }),
isUnlocked: boolean('is_unlocked').default(false),
2026-03-09 11:18:22 +01:00
videos: jsonb().default([]).$type<
{
type: 'youtube' | 'generic' | 'upload';
link: string;
key?: string;
assetId?: string;
/** Display name; stored on add/upload. Fallback: derive from key or type. */
fileName?: string;
metadata?: {
svid?: string;
title?: string;
description?: string;
thumbnailUrl?: string;
duration?: number;
aspectRatio?: string;
createdAt?: string;
};
}[]
>(),
documents: jsonb().default([]).$type<
{
type: string;
name: string;
link: string;
size?: number;
key: string;
assetId?: string;
}[]
>(),
sectionId: uuid('section_id')
2025-11-01 15:49:29 +01:00
},
(table) => [
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'lesson_course_id_fkey'
}),
foreignKey({
columns: [table.teacherId],
foreignColumns: [profile.id],
name: 'lesson_teacher_id_fkey'
}),
foreignKey({
columns: [table.sectionId],
2026-03-09 11:18:22 +01:00
foreignColumns: [courseSection.id],
name: 'public_course_section_id_fkey'
2025-11-01 15:49:29 +01:00
})
.onUpdate('cascade')
.onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
2026-03-09 11:18:22 +01:00
export const asset = pgTable(
'assets',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id').notNull(),
kind: varchar().default('video').notNull(),
provider: varchar().default('upload').notNull(),
storageProvider: varchar('storage_provider').default('s3').notNull(),
storageKey: text('storage_key'),
sourceUrl: text('source_url'),
mimeType: text('mime_type'),
byteSize: bigint('byte_size', { mode: 'number' }),
checksum: text(),
title: text(),
description: text(),
thumbnailUrl: text('thumbnail_url'),
durationSeconds: integer('duration_seconds'),
aspectRatio: text('aspect_ratio'),
isExternal: boolean('is_external').default(false).notNull(),
status: varchar().default('active').notNull(),
metadata: jsonb().default({}),
createdByProfileId: uuid('created_by_profile_id'),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'assets_organization_id_fkey'
})
.onUpdate('cascade')
.onDelete('cascade'),
foreignKey({
columns: [table.createdByProfileId],
foreignColumns: [profile.id],
name: 'assets_created_by_profile_id_fkey'
})
.onUpdate('cascade')
.onDelete('set null'),
index('idx_assets_organization_id').on(table.organizationId),
index('idx_assets_organization_status').on(table.organizationId, table.status),
index('idx_assets_organization_kind_status').on(table.organizationId, table.kind, table.status),
index('idx_assets_organization_created_at').on(table.organizationId, table.createdAt),
index('idx_assets_organization_byte_size').on(table.organizationId, table.byteSize),
unique('assets_org_provider_storage_key_unique').on(table.organizationId, table.provider, table.storageKey)
]
);
export const assetUsage = pgTable(
'asset_usages',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id').notNull(),
assetId: uuid('asset_id').notNull(),
targetType: varchar('target_type').notNull(),
targetId: text('target_id').notNull(),
slotType: varchar('slot_type').default('lesson_video').notNull(),
slotKey: text('slot_key'),
position: integer(),
createdByProfileId: uuid('created_by_profile_id'),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'asset_usages_organization_id_fkey'
})
.onUpdate('cascade')
.onDelete('cascade'),
foreignKey({
columns: [table.assetId],
foreignColumns: [asset.id],
name: 'asset_usages_asset_id_fkey'
})
.onUpdate('cascade')
.onDelete('cascade'),
foreignKey({
columns: [table.createdByProfileId],
foreignColumns: [profile.id],
name: 'asset_usages_created_by_profile_id_fkey'
})
.onUpdate('cascade')
.onDelete('set null'),
index('idx_asset_usages_org_id').on(table.organizationId),
index('idx_asset_usages_asset_id').on(table.assetId),
index('idx_asset_usages_target').on(table.targetType, table.targetId),
unique('asset_usages_asset_target_slot_unique').on(
table.assetId,
table.targetType,
table.targetId,
table.slotType,
table.slotKey,
table.position
)
]
);
2025-11-01 15:49:29 +01:00
export const appsPollSubmission = pgTable(
'apps_poll_submission',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'apps_poll_submision_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
pollOptionId: bigint('poll_option_id', { mode: 'number' }),
selectedById: uuid('selected_by_id'),
pollId: uuid('poll_id')
},
(table) => [
foreignKey({
columns: [table.pollId],
foreignColumns: [appsPoll.id],
name: 'apps_poll_submission_poll_id_fkey'
}),
foreignKey({
columns: [table.pollOptionId],
foreignColumns: [appsPollOption.id],
name: 'apps_poll_submission_poll_option_id_fkey'
}),
foreignKey({
columns: [table.selectedById],
foreignColumns: [groupmember.id],
name: 'apps_poll_submission_selected_by_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const exercise = pgTable(
'exercise',
{
title: varchar().notNull(),
description: varchar(),
lessonId: uuid('lesson_id'),
2026-03-09 11:18:22 +01:00
courseId: uuid('course_id'),
sectionId: uuid('section_id'),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
order: bigint({ mode: 'number' }),
2025-11-01 15:49:29 +01:00
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
2026-03-09 11:18:22 +01:00
isUnlocked: boolean('is_unlocked').default(true),
2025-11-01 15:49:29 +01:00
dueBy: timestamp('due_by', { mode: 'string' })
},
(table) => [
foreignKey({
columns: [table.lessonId],
foreignColumns: [lesson.id],
name: 'exercise_lesson_id_fkey'
2026-03-09 11:18:22 +01:00
}).onDelete('cascade'),
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'exercise_course_id_fkey'
}),
foreignKey({
columns: [table.sectionId],
foreignColumns: [courseSection.id],
name: 'exercise_section_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const groupmember = pgTable(
'groupmember',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
groupId: uuid('group_id').notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
roleId: bigint('role_id', { mode: 'number' }).notNull(),
profileId: uuid('profile_id'),
email: varchar(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
assignedStudentId: varchar('assigned_student_id')
},
(table) => [
foreignKey({
columns: [table.groupId],
foreignColumns: [group.id],
name: 'groupmember_group_id_fkey'
}),
foreignKey({
columns: [table.profileId],
foreignColumns: [profile.id],
name: 'groupmember_profile_id_fkey'
}),
foreignKey({
columns: [table.roleId],
foreignColumns: [role.id],
name: 'groupmember_role_id_fkey'
}),
unique('unique_entries').on(table.groupId, table.profileId, table.email),
unique('unique_group_email').on(table.groupId, table.email),
unique('unique_group_profile').on(table.groupId, table.profileId)
2025-11-01 15:49:29 +01:00
]
);
2026-03-09 11:18:22 +01:00
export const courseInvite = pgTable(
'course_invite',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
courseId: uuid('course_id').notNull(),
// Reserved for future invite-role expansion (defaults to student invites)
roleId: bigint('role_id', { mode: 'number' })
.default(sql`'3'`)
.notNull(),
tokenHash: text('token_hash').notNull(),
createdByProfileId: uuid('created_by_profile_id').notNull(),
revokedByProfileId: uuid('revoked_by_profile_id'),
revokedAt: timestamp('revoked_at', { withTimezone: true, mode: 'string' }),
expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'string' }).notNull(),
maxUses: integer('max_uses').default(1).notNull(),
usedCount: integer('used_count').default(0).notNull(),
isRevoked: boolean('is_revoked').default(false).notNull(),
allowedEmails: text('allowed_emails').array(),
allowedDomains: text('allowed_domains').array(),
metadata: jsonb().default({}).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' })
},
(table) => [
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'course_invite_course_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.createdByProfileId],
foreignColumns: [profile.id],
name: 'course_invite_created_by_profile_id_fkey'
}),
foreignKey({
columns: [table.revokedByProfileId],
foreignColumns: [profile.id],
name: 'course_invite_revoked_by_profile_id_fkey'
}),
foreignKey({
columns: [table.roleId],
foreignColumns: [role.id],
name: 'course_invite_role_id_fkey'
}),
unique('course_invite_token_hash_key').on(table.tokenHash),
index('idx_course_invite_course_id').on(table.courseId),
index('idx_course_invite_expires_at').on(table.expiresAt),
index('idx_course_invite_created_by').on(table.createdByProfileId),
index('idx_course_invite_revoked_by').on(table.revokedByProfileId)
]
);
export const courseInviteAudit = pgTable(
'course_invite_audit',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
inviteId: uuid('invite_id').notNull(),
courseId: uuid('course_id').notNull(),
eventType: courseInviteEventType('event_type').notNull(),
actorProfileId: uuid('actor_profile_id'),
targetEmail: varchar('target_email'),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
metadata: jsonb().default({}).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.inviteId],
foreignColumns: [courseInvite.id],
name: 'course_invite_audit_invite_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'course_invite_audit_course_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.actorProfileId],
foreignColumns: [profile.id],
name: 'course_invite_audit_actor_profile_id_fkey'
}),
index('idx_course_invite_audit_invite_id').on(table.inviteId),
index('idx_course_invite_audit_course_id').on(table.courseId),
index('idx_course_invite_audit_event_type').on(table.eventType),
index('idx_course_invite_audit_created_at').on(table.createdAt)
]
);
export const organizationInvite = pgTable(
'organization_invite',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id').notNull(),
// Role assigned when invite is accepted (e.g ADMIN, TUTOR)
roleId: bigint('role_id', { mode: 'number' }).notNull(),
email: text().notNull(),
tokenHash: text('token_hash').notNull(),
createdByProfileId: uuid('created_by_profile_id').notNull(),
acceptedByProfileId: uuid('accepted_by_profile_id'),
acceptedAt: timestamp('accepted_at', { withTimezone: true, mode: 'string' }),
revokedByProfileId: uuid('revoked_by_profile_id'),
revokedAt: timestamp('revoked_at', { withTimezone: true, mode: 'string' }),
expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'string' }).notNull(),
isRevoked: boolean('is_revoked').default(false).notNull(),
metadata: jsonb().default({}).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'organization_invite_organization_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.roleId],
foreignColumns: [role.id],
name: 'organization_invite_role_id_fkey'
}),
foreignKey({
columns: [table.createdByProfileId],
foreignColumns: [profile.id],
name: 'organization_invite_created_by_profile_id_fkey'
}),
foreignKey({
columns: [table.acceptedByProfileId],
foreignColumns: [profile.id],
name: 'organization_invite_accepted_by_profile_id_fkey'
}),
foreignKey({
columns: [table.revokedByProfileId],
foreignColumns: [profile.id],
name: 'organization_invite_revoked_by_profile_id_fkey'
}),
unique('organization_invite_token_hash_key').on(table.tokenHash),
index('idx_organization_invite_organization_id').on(table.organizationId),
index('idx_organization_invite_email_org').on(table.email, table.organizationId),
index('idx_organization_invite_expires_at').on(table.expiresAt),
index('idx_organization_invite_created_by').on(table.createdByProfileId),
index('idx_organization_invite_accepted_by').on(table.acceptedByProfileId),
index('idx_organization_invite_revoked_by').on(table.revokedByProfileId)
]
);
export const organizationInviteAudit = pgTable(
'organization_invite_audit',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
inviteId: uuid('invite_id').notNull(),
organizationId: uuid('organization_id').notNull(),
eventType: organizationInviteEventType('event_type').notNull(),
actorProfileId: uuid('actor_profile_id'),
targetEmail: varchar('target_email'),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
metadata: jsonb().default({}).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.inviteId],
foreignColumns: [organizationInvite.id],
name: 'organization_invite_audit_invite_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'organization_invite_audit_organization_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.actorProfileId],
foreignColumns: [profile.id],
name: 'organization_invite_audit_actor_profile_id_fkey'
}),
index('idx_organization_invite_audit_invite_id').on(table.inviteId),
index('idx_organization_invite_audit_org_id').on(table.organizationId),
index('idx_organization_invite_audit_event_type').on(table.eventType),
index('idx_organization_invite_audit_created_at').on(table.createdAt)
]
);
2025-11-01 15:49:29 +01:00
export const lessonCompletion = pgTable(
'lesson_completion',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'lesson_completion_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
lessonId: uuid('lesson_id'),
profileId: uuid('profile_id'),
isComplete: boolean('is_complete').default(false),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => [
foreignKey({
columns: [table.lessonId],
foreignColumns: [lesson.id],
name: 'lesson_completion_lesson_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.profileId],
foreignColumns: [profile.id],
name: 'lesson_completion_profile_id_fkey'
}),
unique('unique_lesson_profile').on(table.lessonId, table.profileId)
2025-11-01 15:49:29 +01:00
]
);
export const communityAnswer = pgTable(
'community_answer',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
questionId: bigint('question_id', { mode: 'number' }),
body: varchar(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
authorId: bigint('author_id', { mode: 'number' }),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
votes: bigint({ mode: 'number' }),
authorProfileId: uuid('author_profile_id')
},
(table) => [
foreignKey({
columns: [table.authorId],
foreignColumns: [organizationmember.id],
name: 'community_answer_author_id_fkey'
}),
foreignKey({
columns: [table.authorProfileId],
foreignColumns: [profile.id],
name: 'community_answer_author_profile_id_fkey'
}),
foreignKey({
columns: [table.questionId],
foreignColumns: [communityQuestion.id],
name: 'community_answer_question_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const communityQuestion = pgTable(
'community_question',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'community_question_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
title: varchar(),
body: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
authorId: bigint('author_id', { mode: 'number' }),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
votes: bigint({ mode: 'number' }).default(sql`'0'`),
organizationId: uuid('organization_id'),
slug: text(),
authorProfileId: uuid('author_profile_id'),
courseId: uuid('course_id').notNull()
},
(table) => [
foreignKey({
columns: [table.authorId],
foreignColumns: [organizationmember.id],
name: 'community_question_author_id_fkey'
}),
foreignKey({
columns: [table.authorProfileId],
foreignColumns: [profile.id],
name: 'community_question_author_profile_id_fkey'
}),
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'community_question_course_id_fkey'
}),
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'community_question_organization_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const courseNewsfeed = pgTable(
'course_newsfeed',
{
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
authorId: uuid('author_id'),
content: text(),
id: uuid().defaultRandom().primaryKey().notNull(),
courseId: uuid('course_id'),
2026-03-09 11:18:22 +01:00
reaction: jsonb()
.default({ clap: [], smile: [], thumbsup: [], thumbsdown: [] })
.$type<{ clap: string[]; smile: string[]; thumbsup: string[]; thumbsdown: string[] }>(),
2025-11-01 15:49:29 +01:00
isPinned: boolean('is_pinned').default(false).notNull()
},
(table) => [
foreignKey({
columns: [table.authorId],
foreignColumns: [groupmember.id],
name: 'course_newsfeed_author_id_fkey'
}),
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'course_newsfeed_course_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const courseNewsfeedComment = pgTable(
'course_newsfeed_comment',
{
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
authorId: uuid('author_id'),
content: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'course_newsfeed_comment_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
courseNewsfeedId: uuid('course_newsfeed_id')
},
(table) => [
foreignKey({
columns: [table.authorId],
foreignColumns: [groupmember.id],
name: 'course_newsfeed_comment_author_id_fkey'
}),
foreignKey({
columns: [table.courseNewsfeedId],
foreignColumns: [courseNewsfeed.id],
name: 'course_newsfeed_comment_course_newsfeed_id_fkey'
2025-12-25 05:09:27 +01:00
})
2025-11-01 15:49:29 +01:00
]
);
export const testTenant = pgTable('test_tenant', {
id: serial().primaryKey().notNull(),
details: text()
});
export const organizationPlan = pgTable(
'organization_plan',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'organization_plan_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
activatedAt: timestamp('activated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
orgId: uuid('org_id'),
planName: plan('plan_name'),
isActive: boolean('is_active'),
deactivatedAt: timestamp('deactivated_at', { withTimezone: true, mode: 'string' }),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
payload: jsonb(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
triggeredBy: bigint('triggered_by', { mode: 'number' }),
provider: text().default('lmz'),
subscriptionId: text('subscription_id')
},
(table) => [
foreignKey({
columns: [table.orgId],
foreignColumns: [organization.id],
name: 'organization_plan_org_id_fkey'
}),
foreignKey({
columns: [table.triggeredBy],
foreignColumns: [organizationmember.id],
name: 'organization_plan_triggered_by_fkey'
}),
unique('organization_plan_subscription_id_key').on(table.subscriptionId),
index('idx_organization_plan_org_id').on(table.orgId)
2025-11-01 15:49:29 +01:00
]
);
export const lessonLanguage = pgTable(
'lesson_language',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'lesson_language_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
content: text(),
lessonId: uuid('lesson_id').defaultRandom(),
locale: locale().default('en')
},
(table) => [
foreignKey({
columns: [table.lessonId],
foreignColumns: [lesson.id],
name: 'public_lesson_language_lesson_id_fkey'
})
.onUpdate('cascade')
.onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
export const lessonLanguageHistory = pgTable(
'lesson_language_history',
{
id: serial().primaryKey().notNull(),
lessonLanguageId: integer('lesson_language_id'),
oldContent: text('old_content'),
newContent: text('new_content'),
timestamp: timestamp({ mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.lessonLanguageId],
foreignColumns: [lessonLanguage.id],
name: 'public_lesson_language_history_lesson_language_id_fkey'
})
.onUpdate('cascade')
.onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
export const organizationmember = pgTable(
'organizationmember',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'organizationmember_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
organizationId: uuid('organization_id').notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
roleId: bigint('role_id', { mode: 'number' }).notNull(),
profileId: uuid('profile_id'),
email: text(),
verified: boolean().default(false),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'organizationmember_organization_id_fkey'
}),
foreignKey({
columns: [table.profileId],
foreignColumns: [profile.id],
name: 'organizationmember_profile_id_fkey'
}),
foreignKey({
columns: [table.roleId],
foreignColumns: [role.id],
name: 'organizationmember_role_id_fkey'
}),
index('idx_organizationmember_profile_id').on(table.profileId),
index('idx_organizationmember_organization_id').on(table.organizationId),
index('idx_organizationmember_profile_org').on(table.profileId, table.organizationId)
2025-11-01 15:49:29 +01:00
]
);
export const question = pgTable(
'question',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'question_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
questionTypeId: bigint('question_type_id', { mode: 'number' }).notNull(),
title: varchar().notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
exerciseId: uuid('exercise_id').notNull(),
name: uuid().default(sql`gen_random_uuid()`),
2025-11-01 15:49:29 +01:00
points: doublePrecision(),
2026-03-09 11:18:22 +01:00
settings: jsonb().default({}),
2025-11-01 15:49:29 +01:00
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
order: bigint({ mode: 'number' })
},
(table) => [
foreignKey({
columns: [table.exerciseId],
foreignColumns: [exercise.id],
name: 'question_exercise_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.questionTypeId],
foreignColumns: [questionType.id],
name: 'question_question_type_id_fkey'
})
2025-11-01 15:49:29 +01:00
]
);
export const questionAnswer = pgTable(
'question_answer',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'question_answer_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
2026-03-09 11:18:22 +01:00
answerData: jsonb('answer_data').$type<AnswerData | null>(),
/** @deprecated Use answer_data instead. Kept for backwards compatibility. */
2025-11-01 15:49:29 +01:00
answers: varchar().array(),
2026-03-09 11:18:22 +01:00
/** @deprecated Use answer_data instead. Kept for backwards compatibility. */
openAnswer: text('open_answer'),
2025-11-01 15:49:29 +01:00
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
questionId: bigint('question_id', { mode: 'number' }).notNull(),
groupMemberId: uuid('group_member_id').notNull(),
submissionId: uuid('submission_id'),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
point: bigint({ mode: 'number' }).default(sql`'0'`)
},
(table) => [
foreignKey({
columns: [table.groupMemberId],
foreignColumns: [groupmember.id],
name: 'question_answer_group_member_id_fkey'
}),
foreignKey({
columns: [table.questionId],
foreignColumns: [question.id],
name: 'question_answer_question_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.submissionId],
foreignColumns: [submission.id],
name: 'question_answer_submission_id_fkey'
}).onDelete('cascade')
2025-11-01 15:49:29 +01:00
]
);
export const questionType = pgTable(
'question_type',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'question_type_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
label: varchar().notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
typename: varchar()
},
(table) => []
2025-11-01 15:49:29 +01:00
);
export const role = pgTable(
'role',
{
type: varchar().notNull(),
description: varchar(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'role_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => []
2025-11-01 15:49:29 +01:00
);
export const waitinglist = pgTable(
'waitinglist',
{
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'waitinglist_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
email: varchar().notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow()
},
(table) => [unique('constraint_name').on(table.email)]
);
export const organization = pgTable(
'organization',
{
id: uuid()
.default(sql`gen_random_uuid()`)
2025-11-01 15:49:29 +01:00
.primaryKey()
.notNull(),
name: varchar().notNull(),
siteName: text(),
avatarUrl: text('avatar_url'),
2026-03-09 11:18:22 +01:00
settings: jsonb().default({}).$type<{
signup?: {
inviteOnly?: boolean;
};
}>(),
landingpage: jsonb().default({}).$type<{
header?: {
title: string;
titleHighlight: string;
subtitle: string;
action: {
label: string;
link: string;
redirect: boolean;
};
banner: {
video: string;
image: string;
type: string;
show: boolean;
};
background: {
image: string;
show: boolean;
};
show: boolean;
};
aboutUs?: {
title: string;
content: string;
imageUrl: string;
show: boolean;
};
courses?: {
title: string;
titleHighlight: string;
subtitle: string;
show: boolean;
};
faq?: {
title: string;
questions: Array<{
id: number;
title: string;
content: string;
}>;
show: boolean;
};
contact?: {
title: string;
titleHighlight: string;
subtitle: string;
address: string;
phone: string;
email: string;
show: boolean;
};
mailinglist?: {
title: string;
subtitle: string;
buttonLabel: string;
show: boolean;
};
customLinks?: {
show: boolean;
links: Array<{
id: number;
label: string;
url: string;
openInNewTab: boolean;
}>;
};
footer?: {
facebook: string;
instagram: string;
twitter: string;
linkedin: string;
show: boolean;
};
}>(),
2026-03-09 11:18:22 +01:00
theme: text().default('blue'),
2025-11-01 15:49:29 +01:00
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
customization: json()
.default({
apps: { poll: true, comments: true },
course: { grading: true, newsfeed: true },
dashboard: { exercise: true, community: true, bannerText: '', bannerImage: '' }
})
.$type<{
dashboard: {
community: boolean;
exercise: boolean;
bannerImage: string;
bannerText: string;
};
course: {
newsfeed: boolean;
grading: boolean;
};
apps: {
poll: boolean;
comments: boolean;
};
}>()
2025-11-01 15:49:29 +01:00
.notNull(),
isRestricted: boolean('is_restricted').default(false).notNull(),
customCode: text(),
customDomain: text(),
favicon: text(),
2026-03-09 11:18:22 +01:00
isCustomDomainVerified: boolean().default(false),
disableSignup: boolean('disable_signup').default(false),
disableSignupMessage: text('disable_signup_message'),
disableEmailPassword: boolean('disable_email_password').default(false),
disableGoogleAuth: boolean('disable_google_auth').default(false)
2025-11-01 15:49:29 +01:00
},
(table) => [
unique('organization_siteName_key').on(table.siteName),
unique('organization_customDomain_key').on(table.customDomain)
2025-11-01 15:49:29 +01:00
]
);
2026-03-09 11:18:22 +01:00
export const tagGroup = pgTable(
'tag_group',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id').notNull(),
name: varchar().notNull(),
slug: text().notNull(),
description: text(),
order: integer().default(0).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'tag_group_organization_id_fkey'
}).onDelete('cascade'),
index('idx_tag_group_organization_id').on(table.organizationId),
unique('tag_group_org_slug_key').on(table.organizationId, table.slug)
]
);
export const tag = pgTable(
'tag',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id').notNull(),
groupId: uuid('group_id').notNull(),
name: varchar().notNull(),
slug: text().notNull(),
description: text(),
color: varchar().notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organization.id],
name: 'tag_organization_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.groupId],
foreignColumns: [tagGroup.id],
name: 'tag_group_id_fkey'
}).onDelete('cascade'),
index('idx_tag_organization_id').on(table.organizationId),
index('idx_tag_group_id').on(table.groupId),
unique('tag_org_slug_key').on(table.organizationId, table.slug)
]
);
export const tagAssignment = pgTable(
'tag_assignment',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
tagId: uuid('tag_id').notNull(),
courseId: uuid('course_id').notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.tagId],
foreignColumns: [tag.id],
name: 'tag_assignment_tag_id_fkey'
}).onDelete('cascade'),
foreignKey({
columns: [table.courseId],
foreignColumns: [course.id],
name: 'tag_assignment_course_id_fkey'
}).onDelete('cascade'),
index('idx_tag_assignment_tag_id').on(table.tagId),
index('idx_tag_assignment_course_id').on(table.courseId),
unique('tag_assignment_tag_course_key').on(table.tagId, table.courseId)
]
);
2025-11-01 15:49:29 +01:00
export const dashOrgStats = pgView('dash_org_stats', {
orgId: uuid('org_id'),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
noOfCourses: bigint('no_of_courses', { mode: 'number' }),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
enrolledStudents: bigint('enrolled_students', { mode: 'number' })
}).as(
sql`SELECT gp.organization_id AS org_id, count(DISTINCT course.id) AS no_of_courses, count(DISTINCT gm.profile_id) AS enrolled_students FROM course JOIN "group" gp ON gp.id = course.group_id LEFT JOIN groupmember gm ON gm.group_id = gp.id AND gm.role_id = 3 WHERE course.status = 'ACTIVE'::text GROUP BY gp.organization_id`
);
export const lessonVersions = pgView('lesson_versions', {
oldContent: text('old_content'),
newContent: text('new_content'),
timestamp: timestamp({ mode: 'string' }),
locale: locale(),
lessonId: uuid('lesson_id')
}).as(
sql`SELECT llh.old_content, llh.new_content, llh."timestamp", ll.locale, ll.lesson_id FROM lesson_language_history llh JOIN lesson_language ll ON ll.id = llh.lesson_language_id`
);
2025-12-25 05:09:27 +01:00
export const exerciseTemplate = pgTable('exercise_template', {
id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({
name: 'exercise_template_id_seq',
startWith: 1,
increment: 1,
minValue: 1,
cache: 1
}),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
title: text(),
description: text(),
tag: text(),
questionnaire: jsonb().default({}).$type<{
questions: [
{
title: string;
name: string;
points: number;
order: number;
question_type: {
id: number;
label: string;
};
options: [
{
label: string;
is_correct: boolean;
},
{
label: string;
is_correct: boolean;
},
{
label: string;
is_correct: boolean;
}
];
}
];
}>()
});
2026-03-09 11:18:22 +01:00
// Organization SSO provider type (our app enum for display/categorization)
export const organizationSsoProviderType = pgEnum('SSO_PROVIDER', ['OKTA', 'GOOGLE_WORKSPACE', 'AUTH0']);
// Organization Auth Policy - stores SSO policies per org
export const organizationAuthPolicy = pgTable(
'organization_auth_policy',
{
organizationId: uuid('organization_id')
.notNull()
.primaryKey()
.references(() => organization.id, { onDelete: 'cascade' }),
forceSso: boolean('force_sso').default(false).notNull(),
autoJoinSsoDomains: boolean('auto_join_sso_domains').default(false).notNull(),
breakGlassEnabled: boolean('break_glass_enabled').default(true).notNull(),
defaultRoleId: bigint('default_role_id', { mode: 'number' })
.default(sql`'3'`)
.notNull(),
roleMapping: jsonb('role_mapping').default({}).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
foreignKey({
columns: [table.defaultRoleId],
foreignColumns: [role.id],
name: 'organization_auth_policy_default_role_id_fkey'
}),
index('idx_organization_auth_policy_org_id').on(table.organizationId)
]
);
// Organization SSO Config - metadata for SSO connections (Better Auth stores the actual config)
export const organizationSsoConfig = pgTable(
'organization_sso_config',
{
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id')
.notNull()
.unique()
.references(() => organization.id, { onDelete: 'cascade' }),
betterAuthProviderId: text('better_auth_provider_id').notNull(),
provider: organizationSsoProviderType().notNull(),
displayName: text('display_name').notNull(),
domain: text().notNull(),
isActive: boolean('is_active').default(false).notNull(),
createdByProfileId: uuid('created_by_profile_id')
.notNull()
.references(() => profile.id),
updatedByProfileId: uuid('updated_by_profile_id').references(() => profile.id),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [
index('idx_organization_sso_config_org_id').on(table.organizationId),
index('idx_organization_sso_config_domain').on(table.domain),
index('idx_organization_sso_config_provider_id').on(table.betterAuthProviderId)
]
);
export const organizationTokenAuth = pgTable(
'organization_token_auth',
{
id: uuid('id')
.default(sql`gen_random_uuid()`)
.primaryKey()
.notNull(),
organizationId: uuid('organization_id')
.notNull()
.unique()
.references(() => organization.id, { onDelete: 'cascade' }),
signingSecret: text('signing_secret').notNull(),
isActive: boolean('is_active').default(false).notNull(),
createdByProfileId: uuid('created_by_profile_id')
.notNull()
.references(() => profile.id),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.default(sql`timezone('utc'::text, now())`)
.notNull()
},
(table) => [index('idx_organization_token_auth_org_id').on(table.organizationId)]
);