2025-11-01 15:49:29 +01:00
import {
bigint ,
boolean ,
2026-04-27 11:40:38 +01:00
date ,
2025-11-01 15:49:29 +01:00
doublePrecision ,
foreignKey ,
index ,
integer ,
json ,
jsonb ,
pgEnum ,
pgTable ,
pgView ,
serial ,
text ,
timestamp ,
unique ,
2026-03-12 05:31:46 +01:00
uniqueIndex ,
2025-11-01 15:49:29 +01:00
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' ;
2026-05-09 06:05:03 +01:00
export const courseType = pgEnum ( 'COURSE_TYPE' , [ 'SELF_PACED' , 'LIVE_CLASS' , 'COMPLIANCE' , 'PUBLIC' ]);
2025-11-01 15:49:29 +01:00
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-18 23:39:12 +01:00
export const courseImportSourceType = pgEnum ( 'COURSE_IMPORT_SOURCE_TYPE' , [ 'prompt' , 'pdf' , 'course' ]);
2026-03-13 18:40:07 +01:00
export const courseImportDraftStatus = pgEnum ( 'COURSE_IMPORT_DRAFT_STATUS' , [ 'DRAFT' , 'PUBLISHED' , 'ARCHIVED' ]);
export const organizationApiKeyType = pgEnum ( 'ORGANIZATION_API_KEY_TYPE' , [ 'mcp' , 'api' , 'zapier' ]);
2026-03-18 23:39:12 +01:00
export const automationUsageCategory = pgEnum ( 'AUTOMATION_USAGE_CATEGORY' , [ 'read' , 'write' , 'publish' ]);
2026-04-27 11:40:38 +01:00
export const widgetStatus = pgEnum ( 'WIDGET_STATUS' , [ 'DRAFT' , 'PUBLISHED' , 'ARCHIVED' ]);
export const widgetLayoutType = pgEnum ( 'WIDGET_LAYOUT_TYPE' , [
'card_grid' ,
'tag_filter' ,
'carousel' ,
'primary_course' ,
'compact_list' ,
'editorial_spotlight' ,
'category_shelf'
]);
export const widgetSelectionMode = pgEnum ( 'WIDGET_SELECTION_MODE' , [ 'manual' , 'published' ]);
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
2025-11-24 01:00:50 +01:00
export const user = pgTable ( 'user' , {
id : uuid ()
2025-12-02 09:54:38 +01:00
. default ( sql `gen_random_uuid()` )
2025-11-24 01:00:50 +01:00
. 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 ()
});
2025-11-24 01:00:50 +01:00
export const session = pgTable ( 'session' , {
id : uuid ()
2025-12-02 09:54:38 +01:00
. default ( sql `gen_random_uuid()` )
2025-11-24 01:00:50 +01:00
. 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' )
2025-12-02 09:54:38 +01:00
. default ( sql `gen_random_uuid()` )
2025-11-24 01:00:50 +01:00
. 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' )
2025-12-02 09:54:38 +01:00
. default ( sql `gen_random_uuid()` )
2025-11-24 01:00:50 +01:00
. 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 ()
2025-12-02 09:54:38 +01:00
. default ( sql `gen_random_uuid()` )
2025-11-01 15:49:29 +01:00
. primaryKey ()
. notNull (),
userId : uuid ( 'user_id' ). notNull (),
2026-04-27 11:40:38 +01:00
loggedInAt : timestamp ( 'logged_in_at' , { withTimezone : true , mode : 'string' }). defaultNow (),
loggedInDate : date ( 'logged_in_date' )
. default ( sql `CURRENT_DATE` )
. notNull ()
2025-11-01 15:49:29 +01:00
},
( 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' )),
2026-04-27 11:40:38 +01:00
index ( 'idx_analytics_login_events_logged_in_date' ). using (
'btree' ,
table . loggedInDate . asc (). nullsLast (). op ( 'date_ops' )
),
2025-11-01 15:49:29 +01:00
foreignKey ({
columns : [ table . userId ],
foreignColumns : [ user . id ],
name : 'analytics_login_events_user_id_fkey'
}),
2026-04-27 11:40:38 +01:00
unique ( 'analytics_login_events_user_id_date_unique' ). on ( table . userId , table . loggedInDate )
2025-11-01 15:49:29 +01:00
]
);
2026-05-18 03:47:22 +01:00
export const analyticsPageEvent = pgTable (
'analytics_page_event' ,
{
id : uuid (). defaultRandom (). primaryKey (). notNull (),
occurredAt : timestamp ( 'occurred_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
orgId : uuid ( 'org_id' ),
sessionId : text ( 'session_id' ). notNull (),
userId : uuid ( 'user_id' ),
eventType : text ( 'event_type' ). notNull (),
courseId : uuid ( 'course_id' ),
path : text ( 'path' ),
referrerHost : text ( 'referrer_host' ),
utmSource : text ( 'utm_source' ),
utmMedium : text ( 'utm_medium' ),
utmCampaign : text ( 'utm_campaign' ),
country : varchar ( 'country' , { length : 2 }),
deviceType : varchar ( 'device_type' , { length : 16 }),
locale : varchar ( 'locale' , { length : 8 }),
props : jsonb (). default ({}). notNull ()
},
( table ) => [
index ( 'idx_ape_org_occurred' ). on ( table . orgId , table . occurredAt ),
index ( 'idx_ape_course_occurred' ). on ( table . courseId , table . occurredAt ),
index ( 'idx_ape_session' ). on ( table . sessionId ),
index ( 'idx_ape_event_type' ). on ( table . eventType ),
index ( 'idx_ape_user' ). on ( table . userId )
]
);
export const analyticsOrgDaily = pgTable (
'analytics_org_daily' ,
{
id : uuid (). defaultRandom (). primaryKey (). notNull (),
orgId : uuid ( 'org_id' ). notNull (),
date : date ( 'date' ). notNull (),
landingViews : integer ( 'landing_views' ). default ( 0 ). notNull (),
uniqueVisitors : integer ( 'unique_visitors' ). default ( 0 ). notNull (),
coursePageViews : integer ( 'course_page_views' ). default ( 0 ). notNull (),
enrollments : integer ( 'enrollments' ). default ( 0 ). notNull (),
completions : integer ( 'completions' ). default ( 0 ). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
unique ( 'analytics_org_daily_org_date_unique' ). on ( table . orgId , table . date ),
index ( 'idx_aod_org_date' ). on ( table . orgId , table . date )
]
);
export const analyticsCourseDaily = pgTable (
'analytics_course_daily' ,
{
id : uuid (). defaultRandom (). primaryKey (). notNull (),
courseId : uuid ( 'course_id' ). notNull (),
orgId : uuid ( 'org_id' ). notNull (),
date : date ( 'date' ). notNull (),
views : integer ( 'views' ). default ( 0 ). notNull (),
uniqueVisitors : integer ( 'unique_visitors' ). default ( 0 ). notNull (),
enrollments : integer ( 'enrollments' ). default ( 0 ). notNull (),
completions : integer ( 'completions' ). default ( 0 ). notNull (),
avgTimeToEnrollSeconds : integer ( 'avg_time_to_enroll_seconds' ),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
unique ( 'analytics_course_daily_course_date_unique' ). on ( table . courseId , table . date ),
index ( 'idx_acd_org_date' ). on ( table . orgId , table . date ),
index ( 'idx_acd_course_date' ). on ( table . courseId , table . date )
]
);
export const analyticsCountryDaily = pgTable (
'analytics_country_daily' ,
{
id : uuid (). defaultRandom (). primaryKey (). notNull (),
orgId : uuid ( 'org_id' ). notNull (),
date : date ( 'date' ). notNull (),
country : varchar ( 'country' , { length : 2 }). notNull (),
views : integer ( 'views' ). default ( 0 ). notNull (),
enrollments : integer ( 'enrollments' ). default ( 0 ). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
unique ( 'analytics_country_daily_org_date_country_unique' ). on ( table . orgId , table . date , table . country ),
index ( 'idx_acntd_org_date' ). on ( table . orgId , table . date )
]
);
2026-05-19 04:32:32 +01:00
export const courseSection = pgTable ( 'course_section' , {
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' ). references (() => course . id , {
onDelete : 'cascade' ,
onUpdate : 'cascade'
})
});
2025-11-01 15:49:29 +01:00
export const group = pgTable (
'group' ,
{
id : uuid ()
2025-12-02 09:54:38 +01:00
. 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-12-02 09:54:38 +01:00
})
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-12-02 09:54:38 +01:00
})
2025-11-01 15:49:29 +01:00
]
);
export const profile = pgTable (
'profile' ,
{
id : uuid (). primaryKey (). notNull (),
fullname : text (). notNull (),
username : text (). notNull (),
2026-03-28 23:22:55 +01:00
avatarUrl : text ( 'avatar_url' ),
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 (),
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 ),
2025-11-24 01:00:50 +01:00
index ( 'idx_profile_id' ). on ( table . id ),
2025-12-02 09:54:38 +01:00
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 (),
2025-12-02 09:54:38 +01:00
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'
2025-12-02 09:54:38 +01:00
}). 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'
}),
2025-12-02 09:54:38 +01:00
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'
2025-12-02 09:54:38 +01:00
}). onDelete ( 'cascade' )
2025-11-01 15:49:29 +01:00
]
);
export const quiz = pgTable (
'quiz' ,
{
id : uuid ()
2025-12-02 09:54:38 +01:00
. 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 ()
2025-12-02 09:54:38 +01:00
. 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-12-02 09:54:38 +01:00
})
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 ()
},
2026-05-09 06:05:03 +01:00
() => []
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 ()
2025-12-02 09:54:38 +01:00
. 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 ;
2026-05-18 03:47:22 +01:00
/** Per-course AI tutor override; missing fields inherit from org defaults. */
aiTutor ?: {
inheritFromOrg? : boolean ;
enabled? : boolean ;
persona ?: 'friendly' | 'formal' | 'encouraging' | 'socratic' | 'custom' ;
customPersona? : string ;
responseLength ?: 'short' | 'medium' | 'long' ;
welcomeMessage? : string ;
disclaimerFooter? : string ;
thingsToSay? : string ;
thingsNotToSay? : string ;
forbiddenTopics? : string [];
groundingScope ?: 'lesson' | 'course' | 'workspace' ;
requireCitations? : boolean ;
assessmentMode ?: 'direct_answer' | 'hint_only' | 'block_during_exercise' ;
revealSolutionsAfterAttempts? : number ;
codePolicy ?: 'allowed' | 'hints_only' | 'forbidden' ;
blockOffTopic? : boolean ;
profanityFilter? : boolean ;
escalation ?: { enabled? : boolean ; email? : string };
};
2026-03-09 11:18:22 +01:00
} > (),
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 ),
2026-03-30 08:24:55 +01:00
certificate : jsonb (). default ({}). $type < {
isDownloadable? : boolean ;
2026-05-18 03:47:22 +01:00
/** @deprecated Use `design.templateId`. Legacy 6-theme id; mapped on read via LEGACY_THEME_MAP. */
2026-03-30 08:24:55 +01:00
theme? : string ;
2026-05-18 03:47:22 +01:00
/** Atelier-era certificate design. Source of truth for new courses. */
design ?: {
templateId : 'classique' | 'brutalist' | 'noir' | 'poster' | 'minimal' ;
accentColor : string ;
subtitle? : string ;
descriptionOverride? : string ;
signatories : [{ name : string ; role : string }, { name : string ; role : string }];
idFormat? : string ;
};
2026-03-30 08:24:55 +01:00
deadline? : string | null ;
threshold? : number ;
requiredExerciseId? : string | null ;
exerciseMinScorePercent? : number | null ;
emailMessage? : string | null ;
} > (),
2026-04-27 11:40:38 +01:00
compliance : jsonb (). $type < {
retakeIntervalMonths : number ;
gracePeriodDays? : number ;
reminderDaysBefore? : number [];
isMandatory? : boolean ;
framework ?: 'HIPAA' | 'OSHA' | 'SOX' | 'GDPR' | 'PCI_DSS' | 'FERPA' | 'ISO' | 'CUSTOM' | null ;
maxRetakeAttempts? : number | null ;
passingScore? : number ;
} > (),
2025-11-01 15:49:29 +01:00
status : text (). default ( 'ACTIVE' ). notNull (),
2026-05-09 06:05:03 +01:00
type : courseType (). default ( 'SELF_PACED' ),
callout : jsonb (). $type < {
title : string ;
description : string ;
buttonLabel : string ;
buttonUrl : string ;
2026-05-18 03:47:22 +01:00
animation ?: 'waves' | 'dotted' | 'none' ;
2026-05-09 06:05:03 +01:00
} | null > ()
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
]
);
2026-04-27 11:40:38 +01:00
export const courseCompletionRecord = pgTable (
'course_completion_record' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
courseId : uuid ( 'course_id' ). notNull (),
groupMemberId : uuid ( 'group_member_id' ). notNull (),
profileId : uuid ( 'profile_id' ). notNull (),
cycleNumber : integer ( 'cycle_number' ). default ( 1 ). notNull (),
status : varchar (). default ( 'not_started' ). notNull (),
dueDate : timestamp ( 'due_date' , { withTimezone : true , mode : 'string' }). notNull (),
startedAt : timestamp ( 'started_at' , { withTimezone : true , mode : 'string' }),
completedAt : timestamp ( 'completed_at' , { withTimezone : true , mode : 'string' }),
validUntil : timestamp ( 'valid_until' , { withTimezone : true , mode : 'string' }),
expiredAt : timestamp ( 'expired_at' , { withTimezone : true , mode : 'string' }),
score : integer (),
attempts : integer (). default ( 0 ),
timeSpentMinutes : integer ( 'time_spent_minutes' ). default ( 0 ),
waivedBy : uuid ( 'waived_by' ),
waiverReason : text ( 'waiver_reason' ),
waiverExpiresAt : timestamp ( 'waiver_expires_at' , { withTimezone : true , mode : 'string' }),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . courseId ],
foreignColumns : [ course . id ],
name : 'course_completion_record_course_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . groupMemberId ],
foreignColumns : [ groupmember . id ],
name : 'course_completion_record_group_member_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . profileId ],
foreignColumns : [ profile . id ],
name : 'course_completion_record_profile_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . waivedBy ],
foreignColumns : [ profile . id ],
name : 'course_completion_record_waived_by_fkey'
}). onDelete ( 'set null' ),
unique ( 'course_completion_record_course_profile_cycle_key' ). on ( table . courseId , table . profileId , table . cycleNumber ),
index ( 'idx_course_completion_record_course_id' ). on ( table . courseId ),
index ( 'idx_course_completion_record_profile_id' ). on ( table . profileId ),
index ( 'idx_course_completion_record_status' ). on ( table . status ),
index ( 'idx_course_completion_record_due_date' ). on ( table . dueDate ),
index ( 'idx_course_completion_record_valid_until' ). on ( table . validUntil )
]
);
export const courseCompletionNotificationEvent = pgTable (
'course_completion_notification_event' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
courseCompletionRecordId : uuid ( 'course_completion_record_id' ). notNull (),
channel : varchar (). notNull (),
eventType : varchar ( 'event_type' ). notNull (),
sentAt : timestamp ( 'sent_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . courseCompletionRecordId ],
foreignColumns : [ courseCompletionRecord . id ],
name : 'course_completion_notification_event_record_id_fkey'
}). onDelete ( 'cascade' ),
unique ( 'course_completion_notification_event_record_channel_type_key' ). on (
table . courseCompletionRecordId ,
table . channel ,
table . eventType
)
]
);
export const courseCertificateIssue = pgTable (
'course_certificate_issue' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
courseId : uuid ( 'course_id' ). notNull (),
profileId : uuid ( 'profile_id' ). notNull (),
courseCompletionRecordId : uuid ( 'course_completion_record_id' ). notNull (),
cycleNumber : integer ( 'cycle_number' ). notNull (),
issuedAt : timestamp ( 'issued_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
expiresAt : timestamp ( 'expires_at' , { withTimezone : true , mode : 'string' }). notNull (),
status : varchar (). default ( 'valid' ). notNull (),
fileUrl : text ( 'file_url' )
},
( table ) => [
foreignKey ({
columns : [ table . courseId ],
foreignColumns : [ course . id ],
name : 'course_certificate_issue_course_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . profileId ],
foreignColumns : [ profile . id ],
name : 'course_certificate_issue_profile_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . courseCompletionRecordId ],
foreignColumns : [ courseCompletionRecord . id ],
name : 'course_certificate_issue_record_id_fkey'
}). onDelete ( 'cascade' ),
unique ( 'course_certificate_issue_record_id_key' ). on ( table . courseCompletionRecordId ),
index ( 'idx_course_certificate_issue_course_id' ). on ( table . courseId ),
index ( 'idx_course_certificate_issue_profile_id' ). on ( table . profileId ),
index ( 'idx_course_certificate_issue_status' ). on ( table . status )
]
);
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-12-02 09:54:38 +01:00
})
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 ()
2025-12-02 09:54:38 +01:00
. 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 <
{
2026-04-12 08:58:41 +01:00
type : 'youtube' | 'generic' | 'upload' | 'google_drive' ;
2026-03-09 11:18:22 +01:00
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 ;
}[]
> (),
2026-05-19 04:32:32 +01:00
sectionId : uuid ( 'section_id' ). references (() => courseSection . id , {
onDelete : 'cascade' ,
onUpdate : 'cascade'
}),
2026-05-09 06:05:03 +01:00
slug : varchar ()
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'
}),
2026-05-09 06:05:03 +01:00
index ( 'idx_lesson_course_slug' ). on ( table . courseId , table . slug )
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-12-02 09:54:38 +01:00
})
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 ()
2025-12-02 09:54:38 +01:00
. 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 ),
2026-03-28 23:22:55 +01:00
dueBy : timestamp ( 'due_by' , { mode : 'string' }),
2026-05-09 06:05:03 +01:00
allowMultipleAttempts : boolean ( 'allow_multiple_attempts' ). default ( false ). notNull (),
sectionDisplayMode : varchar ( 'section_display_mode' ). default ( 'one_question' ),
slug : varchar ()
2025-11-01 15:49:29 +01:00
},
( 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'
2026-05-09 06:05:03 +01:00
}),
index ( 'idx_exercise_course_slug' ). on ( table . courseId , table . slug )
]
);
export const exerciseSection = pgTable (
'exercise_section' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
exerciseId : uuid ( 'exercise_id' ). notNull (),
title : varchar (). notNull (). default ( 'Untitled Section' ),
description : text (),
order : bigint ({ mode : 'number' }). notNull (). default ( 0 ),
colorTheme : varchar ( 'color_theme' ). notNull (). default ( 'blue' ),
afterBehavior : jsonb ( 'after_behavior' ). notNull (). default ({ action : 'continue' }),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow ()
},
( table ) => [
foreignKey ({
columns : [ table . exerciseId ],
foreignColumns : [ exercise . id ],
name : 'exercise_section_exercise_id_fkey'
}). onDelete ( 'cascade' ),
index ( 'idx_exercise_section_exercise_id' ). on ( table . exerciseId )
2025-11-01 15:49:29 +01:00
]
);
export const groupmember = pgTable (
'groupmember' ,
{
id : uuid ()
2025-12-02 09:54:38 +01:00
. 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 (),
2026-03-28 23:22:55 +01:00
assignedStudentId : varchar ( 'assigned_student_id' ),
certificateEarnedAt : timestamp ( 'certificate_earned_at' , { withTimezone : true , mode : 'string' }),
certificationEmailSentAt : timestamp ( 'certification_email_sent_at' , { withTimezone : true , mode : 'string' })
2025-11-01 15:49:29 +01:00
},
( 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 ),
2025-12-02 09:54:38 +01:00
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'
}),
2025-12-02 09:54:38 +01:00
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 ()
2025-12-02 09:54:38 +01:00
. 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-12-02 09:54:38 +01:00
})
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-12-02 09:54:38 +01:00
})
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-12-02 09:54:38 +01:00
})
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 ),
2025-12-02 09:54:38 +01:00
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' )
2025-12-02 09:54:38 +01:00
. 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' )
2025-12-02 09:54:38 +01:00
. 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'
}),
2025-11-24 01:00:50 +01:00
index ( 'idx_organizationmember_profile_id' ). on ( table . profileId ),
index ( 'idx_organizationmember_organization_id' ). on ( table . organizationId ),
2026-03-12 05:31:46 +01:00
index ( 'idx_organizationmember_profile_org' ). on ( table . profileId , table . organizationId ),
uniqueIndex ( 'organizationmember_org_profile_unique' )
. on ( table . organizationId , table . profileId )
2026-03-22 18:42:23 +01:00
. where ( sql ` ${ table . profileId } IS NOT NULL` ),
uniqueIndex ( 'organizationmember_org_email_unique' )
. on ( table . organizationId , table . email )
. where ( sql ` ${ table . email } IS NOT NULL` )
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 (),
2026-05-09 06:05:03 +01:00
exerciseSectionId : uuid ( 'exercise_section_id' ),
2025-12-02 09:54:38 +01:00
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' ),
2026-05-09 06:05:03 +01:00
foreignKey ({
columns : [ table . exerciseSectionId ],
foreignColumns : [ exerciseSection . id ],
name : 'question_exercise_section_id_fkey'
}). onDelete ( 'set null' ),
2025-11-01 15:49:29 +01:00
foreignKey ({
columns : [ table . questionTypeId ],
foreignColumns : [ questionType . id ],
name : 'question_question_type_id_fkey'
2025-12-02 09:54:38 +01:00
})
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'
2025-12-02 09:54:38 +01:00
}). onDelete ( 'cascade' )
2025-11-01 15:49:29 +01:00
]
);
2026-04-12 08:58:41 +01:00
/**
* Exercise question kinds (`typename` matches `@cio/question-types` keys). Expected ids:
* 1 RADIO, 2 CHECKBOX, 3 TEXTAREA, 4 TRUE_FALSE, 5 SHORT_ANSWER, 6 NUMERIC, 7 FILL_BLANK,
2026-05-09 06:05:03 +01:00
* 8 FILE_UPLOAD, 9 MATCHING, 10 ORDERING, 11 HOTSPOT, 12 LINK, 13 WORD_BANK, 14 STAR,
* 15 VIDEO_RECORDING.
2026-04-12 08:58:41 +01:00
*/
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 ()
},
2026-05-09 06:05:03 +01:00
() => []
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 ()
},
2026-05-09 06:05:03 +01:00
() => []
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 ()
2025-12-02 09:54:38 +01:00
. 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 ;
};
} > (),
2025-12-02 09:54:38 +01:00
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 ;
} > ;
};
2026-05-18 03:47:22 +01:00
/** Legacy social URLs-only footer block */
footer ?:
| {
facebook? : string ;
instagram? : string ;
twitter? : string ;
linkedin? : string ;
show? : boolean ;
}
| {
brand : {
tagline? : string ;
copyright? : string ;
socials : Array < { platform : string ; href : string } > ;
};
columns : Array < {
id : string ;
heading : string ;
links : Array < { label : string ; href : string } > ;
cta ?: { label : string ; href : string };
} > ;
bottom ?: {
text? : string ;
links : Array < { label : string ; href : string } > ;
};
};
/** Current landing JSON shape (stored alongside legacy keys above) */
theme? : string ;
hero? : Record < string , unknown >;
navItems? : Array < { label : string ; href : string } > ;
footerLinks? : Array < { label : string ; href : string } > ;
footerText? : string ;
embed? : Record < string , unknown >;
callout? : Record < string , unknown >;
links? : Record < string , unknown >;
2025-12-02 09:54:38 +01:00
} > (),
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 : '' }
})
2025-12-02 09:54:38 +01:00
. $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 ),
2026-05-18 03:47:22 +01:00
disableGoogleAuth : boolean ( 'disable_google_auth' ). default ( false ),
parentOrganizationId : uuid ( 'parent_organization_id' ),
readOnlyUntil : timestamp ( 'read_only_until' , { withTimezone : true , mode : 'string' }),
aiTutorSettings : jsonb ( 'ai_tutor_settings' )
. default ({
enabled : true ,
persona : 'encouraging' ,
customPersona : '' ,
responseLength : 'medium' ,
welcomeMessage : '' ,
disclaimerFooter : 'I am an AI tutor, not your instructor.' ,
thingsToSay : '' ,
thingsNotToSay : '' ,
forbiddenTopics : [],
groundingScope : 'course' ,
requireCitations : true ,
assessmentMode : 'hint_only' ,
revealSolutionsAfterAttempts : 3 ,
codePolicy : 'hints_only' ,
blockOffTopic : true ,
profanityFilter : true ,
escalation : { enabled : false , email : '' }
})
. $type < {
enabled : boolean ;
persona : 'friendly' | 'formal' | 'encouraging' | 'socratic' | 'custom' ;
customPersona : string ;
responseLength : 'short' | 'medium' | 'long' ;
welcomeMessage : string ;
disclaimerFooter : string ;
thingsToSay : string ;
thingsNotToSay : string ;
forbiddenTopics : string [];
groundingScope : 'lesson' | 'course' | 'workspace' ;
requireCitations : boolean ;
assessmentMode : 'direct_answer' | 'hint_only' | 'block_during_exercise' ;
revealSolutionsAfterAttempts : number ;
codePolicy : 'allowed' | 'hints_only' | 'forbidden' ;
blockOffTopic : boolean ;
profanityFilter : boolean ;
escalation : { enabled : boolean ; email : string };
} > ()
. notNull ()
2025-11-01 15:49:29 +01:00
},
( table ) => [
unique ( 'organization_siteName_key' ). on ( table . siteName ),
2026-05-18 03:47:22 +01:00
unique ( 'organization_customDomain_key' ). on ( table . customDomain ),
foreignKey ({
columns : [ table . parentOrganizationId ],
foreignColumns : [ table . id ],
name : 'organization_parent_organization_id_fkey'
}). onDelete ( 'set null' ),
index ( 'idx_organization_parent_id' ). on ( table . parentOrganizationId )
2025-11-01 15:49:29 +01:00
]
);
2025-12-02 09:54:38 +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 )
]
);
2026-04-27 11:40:38 +01:00
export const widget = pgTable (
'widget' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' ). notNull (),
name : varchar ({ length : 120 }). notNull (),
status : widgetStatus ( 'status' ). default ( 'DRAFT' ). notNull (),
layoutType : widgetLayoutType ( 'layout_type' ). default ( 'card_grid' ). notNull (),
selectionMode : widgetSelectionMode ( 'selection_mode' ). default ( 'manual' ). notNull (),
publicKey : varchar ( 'public_key' , { length : 64 }). notNull (),
config : jsonb ( 'config' )
. default ( sql `'{}'::jsonb` )
. notNull (),
hasUnpublishedChanges : boolean ( 'has_unpublished_changes' ). default ( false ). notNull (),
latestPublishedVersionId : uuid ( 'latest_published_version_id' ),
createdByUserId : uuid ( 'created_by_user_id' ). notNull (),
updatedByUserId : uuid ( 'updated_by_user_id' ),
deletedAt : timestamp ( 'deleted_at' , { withTimezone : true , mode : 'string' }),
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 : 'widget_organization_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . createdByUserId ],
foreignColumns : [ user . id ],
name : 'widget_created_by_user_id_fkey'
}). onDelete ( 'restrict' ),
foreignKey ({
columns : [ table . updatedByUserId ],
foreignColumns : [ user . id ],
name : 'widget_updated_by_user_id_fkey'
}). onDelete ( 'set null' ),
index ( 'idx_widget_organization_id' ). on ( table . organizationId ),
index ( 'idx_widget_status' ). on ( table . status ),
unique ( 'widget_public_key_key' ). on ( table . publicKey )
]
);
export const widgetCourse = pgTable (
'widget_course' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
widgetId : uuid ( 'widget_id' ). notNull (),
courseId : uuid ( 'course_id' ). notNull (),
order : integer (). default ( 0 ). notNull (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' })
. default ( sql `timezone('utc'::text, now())` )
. notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . widgetId ],
foreignColumns : [ widget . id ],
name : 'widget_course_widget_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . courseId ],
foreignColumns : [ course . id ],
name : 'widget_course_course_id_fkey'
}). onDelete ( 'cascade' ),
index ( 'idx_widget_course_widget_id' ). on ( table . widgetId ),
index ( 'idx_widget_course_course_id' ). on ( table . courseId ),
unique ( 'widget_course_widget_course_key' ). on ( table . widgetId , table . courseId )
]
);
export const widgetVersion = pgTable (
'widget_version' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
widgetId : uuid ( 'widget_id' ). notNull (),
version : integer (). notNull (),
configSnapshot : jsonb ( 'config_snapshot' )
. default ( sql `'{}'::jsonb` )
. notNull (),
payloadSnapshot : jsonb ( 'payload_snapshot' )
. default ( sql `'{}'::jsonb` )
. notNull (),
runtimeManifest : jsonb ( 'runtime_manifest' )
. default ( sql `'{}'::jsonb` )
. notNull (),
rolledBackFromVersionId : uuid ( 'rolled_back_from_version_id' ),
publishedByUserId : uuid ( 'published_by_user_id' ). notNull (),
publishedAt : timestamp ( 'published_at' , { withTimezone : true , mode : 'string' })
. default ( sql `timezone('utc'::text, now())` )
. notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . widgetId ],
foreignColumns : [ widget . id ],
name : 'widget_version_widget_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . publishedByUserId ],
foreignColumns : [ user . id ],
name : 'widget_version_published_by_user_id_fkey'
}). onDelete ( 'restrict' ),
foreignKey ({
columns : [ table . rolledBackFromVersionId ],
foreignColumns : [ table . id ],
name : 'widget_version_rolled_back_from_version_id_fkey'
}). onDelete ( 'set null' ),
index ( 'idx_widget_version_widget_id' ). on ( table . widgetId ),
unique ( 'widget_version_widget_version_key' ). on ( table . widgetId , table . version )
]
);
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 )]
);
2026-03-13 18:40:07 +01:00
export const organizationApiKey = pgTable (
'organization_api_key' ,
{
id : uuid ( 'id' )
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' )
. notNull ()
. references (() => organization . id , { onDelete : 'cascade' }),
createdByProfileId : uuid ( 'created_by_profile_id' )
. notNull ()
. references (() => profile . id ),
type : organizationApiKeyType ( 'type' ). notNull (),
label : varchar ( 'label' , { length : 120 }). notNull (),
secretPrefix : varchar ( 'secret_prefix' , { length : 32 }). notNull (),
secretHash : text ( 'secret_hash' ). notNull (),
scopes : jsonb ( 'scopes' ). default ([]). notNull (). $type < string [] >(),
lastUsedAt : timestamp ( 'last_used_at' , { withTimezone : true , mode : 'string' }),
expiresAt : timestamp ( 'expires_at' , { withTimezone : true , mode : 'string' }),
revokedAt : timestamp ( 'revoked_at' , { withTimezone : true , mode : 'string' }),
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_api_key_org_id' ). on ( table . organizationId ),
index ( 'idx_organization_api_key_type' ). on ( table . type ),
uniqueIndex ( 'organization_api_key_secret_hash_unique' ). on ( table . secretHash )
]
);
2026-03-18 23:39:12 +01:00
export const organizationAutomationUsage = pgTable (
'organization_automation_usage' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' )
. notNull ()
. references (() => organization . id ),
organizationApiKeyId : uuid ( 'organization_api_key_id' ). references (() => organizationApiKey . id , {
onDelete : 'set null'
}),
type : organizationApiKeyType ( 'type' ). notNull (),
action : varchar ( 'action' , { length : 120 }). notNull (),
category : automationUsageCategory ( 'category' ). notNull (),
creditsConsumed : integer ( 'credits_consumed' ). default ( 0 ). notNull (),
metadata : jsonb ( 'metadata' ). default ({}). $type < Record < string , unknown >>(). notNull (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' })
. default ( sql `timezone('utc'::text, now())` )
. notNull ()
},
( table ) => [
index ( 'idx_org_automation_usage_org_created_at' ). on ( table . organizationId , table . createdAt ),
index ( 'idx_org_automation_usage_key_created_at' ). on ( table . organizationApiKeyId , table . createdAt ),
index ( 'idx_org_automation_usage_type_created_at' ). on ( table . type , table . createdAt )
]
);
2026-03-13 18:40:07 +01:00
export const courseImportDraft = pgTable (
'course_import_draft' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' )
. notNull ()
. references (() => organization . id , { onDelete : 'cascade' }),
createdByProfileId : uuid ( 'created_by_profile_id' )
. notNull ()
. references (() => profile . id ),
sourceType : courseImportSourceType ( 'source_type' ). notNull (),
status : courseImportDraftStatus ( 'status' ). default ( 'DRAFT' ). notNull (),
title : varchar (). notNull (),
locale : locale (). default ( 'en' ). notNull (),
idempotencyKey : varchar ( 'idempotency_key' ),
summary : jsonb (). default ({}). notNull (). $type < Record < string , unknown >>(),
draft : jsonb (). notNull (). $type < Record < string , unknown >>(),
warnings : jsonb (). default ([]). notNull (). $type < Array < Record < string , unknown >>>(),
sourceArtifacts : jsonb (). default ([]). notNull (). $type < Array < Record < string , unknown >>>(),
publishedCourseId : uuid ( 'published_course_id' ). references (() => course . id , { onDelete : 'set null' }),
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_course_import_draft_org_id' ). on ( table . organizationId ),
index ( 'idx_course_import_draft_status' ). on ( table . status ),
uniqueIndex ( 'course_import_draft_org_idempotency_key' )
. on ( table . organizationId , table . idempotencyKey )
. where ( sql ` ${ table . idempotencyKey } IS NOT NULL` )
]
);
2026-04-12 08:58:41 +01:00
// ─── Programs ────────────────────────────────────────────────────────────────
export const programStatus = pgEnum ( 'PROGRAM_STATUS' , [ 'ACTIVE' , 'INACTIVE' , 'ARCHIVED' ]);
export const program = pgTable (
'program' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (),
organizationId : uuid ( 'organization_id' ). notNull (),
name : varchar (). notNull (),
description : text (),
coverImage : text ( 'cover_image' ),
status : programStatus (). default ( 'ACTIVE' ). notNull (),
createdByProfileId : uuid ( 'created_by_profile_id' )
},
( table ) => [
foreignKey ({
columns : [ table . organizationId ],
foreignColumns : [ organization . id ],
name : 'program_organization_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . createdByProfileId ],
foreignColumns : [ profile . id ],
name : 'program_created_by_profile_id_fkey'
}),
index ( 'idx_program_organization_id' ). on ( table . organizationId )
]
);
export const programCourse = pgTable (
'program_course' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
programId : uuid ( 'program_id' ). notNull (),
courseId : uuid ( 'course_id' ). notNull (),
addedAt : timestamp ( 'added_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . programId ],
foreignColumns : [ program . id ],
name : 'program_course_program_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . courseId ],
foreignColumns : [ course . id ],
name : 'program_course_course_id_fkey'
}). onDelete ( 'cascade' ),
unique ( 'program_course_program_id_course_id_unique' ). on ( table . programId , table . courseId ),
index ( 'idx_program_course_program_id' ). on ( table . programId )
]
);
export const programMember = pgTable (
'program_member' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
programId : uuid ( 'program_id' ). notNull (),
profileId : uuid ( 'profile_id' ),
roleId : integer ( 'role_id' ). notNull (),
email : text (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . programId ],
foreignColumns : [ program . id ],
name : 'program_member_program_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . profileId ],
foreignColumns : [ profile . id ],
name : 'program_member_profile_id_fkey'
}),
foreignKey ({
columns : [ table . roleId ],
foreignColumns : [ role . id ],
name : 'program_member_role_id_fkey'
}),
unique ( 'program_member_program_id_profile_id_unique' ). on ( table . programId , table . profileId ),
index ( 'idx_program_member_program_id' ). on ( table . programId ),
index ( 'idx_program_member_profile_id' ). on ( table . profileId )
]
);
export const programNewsfeed = pgTable (
'program_newsfeed' ,
{
id : uuid (). defaultRandom (). primaryKey (). notNull (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
authorId : uuid ( 'author_id' ),
programId : uuid ( 'program_id' ),
content : text (),
reaction : jsonb ()
. default ({ clap : [], smile : [], thumbsup : [], thumbsdown : [] })
. $type < { clap : string []; smile : string []; thumbsup : string []; thumbsdown : string [] } > (),
isPinned : boolean ( 'is_pinned' ). default ( false ). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . authorId ],
foreignColumns : [ programMember . id ],
name : 'program_newsfeed_author_id_fkey'
}),
foreignKey ({
columns : [ table . programId ],
foreignColumns : [ program . id ],
name : 'program_newsfeed_program_id_fkey'
}). onDelete ( 'cascade' ),
index ( 'idx_program_newsfeed_program_id' ). on ( table . programId )
]
);
export const programNewsfeedComment = pgTable (
'program_newsfeed_comment' ,
{
id : bigint ({ mode : 'number' }). primaryKey (). generatedByDefaultAsIdentity ({
name : 'program_newsfeed_comment_id_seq' ,
startWith : 1 ,
increment : 1 ,
minValue : 1 ,
cache : 1
}),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
authorId : uuid ( 'author_id' ),
content : text (),
programNewsfeedId : uuid ( 'program_newsfeed_id' )
},
( table ) => [
foreignKey ({
columns : [ table . authorId ],
foreignColumns : [ programMember . id ],
name : 'program_newsfeed_comment_author_id_fkey'
}),
foreignKey ({
columns : [ table . programNewsfeedId ],
foreignColumns : [ programNewsfeed . id ],
name : 'program_newsfeed_comment_program_newsfeed_id_fkey'
}). onDelete ( 'cascade' )
]
);
2026-04-27 11:40:38 +01:00
2026-05-18 03:47:22 +01:00
// ─── Program Goals ────────────────────────────────────────────────────────────
//
// A team-level "is the team done" target. A small generalisation of the
// per-course `course.compliance` field: applies across multiple courses, scoped
// to a program's members, and aggregated for owner roll-ups. Per-learner status
// is materialised in `programGoalAssignment` so dashboards stay cheap.
export const programGoalType = pgEnum ( 'PROGRAM_GOAL_TYPE' , [
'complete_all' ,
'n_of_m' ,
'score' ,
'pass_rate' ,
'readiness'
]);
export const programGoalDeadlineKind = pgEnum ( 'PROGRAM_GOAL_DEADLINE_KIND' , [
'absolute' ,
'relative_to_join' ,
'recurring' ,
'none'
]);
export const programGoalStatus = pgEnum ( 'PROGRAM_GOAL_STATUS' , [ 'active' , 'archived' ]);
export const programGoalAssignmentStatus = pgEnum ( 'PROGRAM_GOAL_ASSIGNMENT_STATUS' , [
'not_started' ,
'in_progress' ,
'completed' ,
'at_risk' ,
'overdue' ,
'waived'
]);
export const programGoal = pgTable (
'program_goal' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
programId : uuid ( 'program_id' ). notNull (),
title : varchar ({ length : 255 }). notNull (),
description : text (),
type : programGoalType (). notNull (),
/** Course IDs the goal targets. JSONB so we can attach per-course config later if needed. */
courseIds : jsonb ( 'course_ids' ). $type < string [] >(). default ([]). notNull (),
/** Required only for n_of_m. */
requiredCount : integer ( 'required_count' ),
/** Per-learner score threshold (0– 100). Required for `score` and `pass_rate`. */
scoreThreshold : integer ( 'score_threshold' ),
/** Team-wide pass-rate threshold (0– 100). Required for `pass_rate`. */
teamPassRateThreshold : integer ( 'team_pass_rate_threshold' ),
deadlineKind : programGoalDeadlineKind ( 'deadline_kind' ). default ( 'none' ). notNull (),
/** Used when deadlineKind = 'absolute'. */
deadlineDate : timestamp ( 'deadline_date' , { withTimezone : true , mode : 'string' }),
/** Used when deadlineKind = 'relative_to_join'. */
relativeDays : integer ( 'relative_days' ),
/** Used when deadlineKind = 'recurring'. */
recurringMonths : integer ( 'recurring_months' ),
/** Reminder cadence (days before due). Mirrors `course.compliance.reminderDaysBefore`. */
reminderDaysBefore : jsonb ( 'reminder_days_before' ). $type < number [] >(). default ([ 7 , 1 ]). notNull (),
status : programGoalStatus (). default ( 'active' ). notNull (),
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 (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . programId ],
foreignColumns : [ program . id ],
name : 'program_goal_program_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . createdByProfileId ],
foreignColumns : [ profile . id ],
name : 'program_goal_created_by_profile_id_fkey'
}). onDelete ( 'set null' ),
index ( 'idx_program_goal_program_id' ). on ( table . programId ),
index ( 'idx_program_goal_status' ). on ( table . status )
]
);
export const programGoalAssignment = pgTable (
'program_goal_assignment' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
goalId : uuid ( 'goal_id' ). notNull (),
programMemberId : uuid ( 'program_member_id' ). notNull (),
/** Resolved due date for this learner (null when goal has no deadline). */
dueDate : timestamp ( 'due_date' , { withTimezone : true , mode : 'string' }),
status : programGoalAssignmentStatus (). default ( 'not_started' ). notNull (),
/** Cached count of completed required courses (for n_of_m / complete_all dashboards). */
completedCount : integer ( 'completed_count' ). default ( 0 ). notNull (),
/** Cached numerator for the requirement (e.g. courses required to complete). */
requiredCount : integer ( 'required_count' ). default ( 0 ). notNull (),
completedAt : timestamp ( 'completed_at' , { withTimezone : true , mode : 'string' }),
lastEvaluatedAt : timestamp ( 'last_evaluated_at' , { withTimezone : true , mode : 'string' }),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . goalId ],
foreignColumns : [ programGoal . id ],
name : 'program_goal_assignment_goal_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . programMemberId ],
foreignColumns : [ programMember . id ],
name : 'program_goal_assignment_program_member_id_fkey'
}). onDelete ( 'cascade' ),
unique ( 'program_goal_assignment_goal_id_program_member_id_unique' ). on ( table . goalId , table . programMemberId ),
index ( 'idx_program_goal_assignment_goal_id' ). on ( table . goalId ),
index ( 'idx_program_goal_assignment_program_member_id' ). on ( table . programMemberId ),
index ( 'idx_program_goal_assignment_due_date' ). on ( table . dueDate )
]
);
2026-04-27 11:40:38 +01:00
// ─── AI Token Usage ──────────────────────────────────────────────────────────
export const aiTokenUsage = pgTable (
'ai_token_usage' ,
{
id : bigint ({ mode : 'number' }). primaryKey (). generatedByDefaultAsIdentity ({
name : 'ai_token_usage_id_seq' ,
startWith : 1 ,
increment : 1 ,
minValue : 1 ,
cache : 1
}),
orgId : uuid ( 'org_id' ). notNull (),
userId : uuid ( 'user_id' ). notNull (),
courseId : uuid ( 'course_id' ). notNull (),
promptTokens : integer ( 'prompt_tokens' ). notNull (),
completionTokens : integer ( 'completion_tokens' ). notNull (),
2026-05-09 06:05:03 +01:00
costUnits : integer ( 'cost_units' ),
2026-04-27 11:40:38 +01:00
model : text (). notNull (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . orgId ],
foreignColumns : [ organization . id ],
name : 'ai_token_usage_org_id_fkey'
}),
index ( 'idx_ai_token_usage_org_month' ). on ( table . orgId , table . createdAt )
]
);
// ─── AI Credit Balance ───────────────────────────────────────────────────────
export const aiCreditBalance = pgTable (
'ai_credit_balance' ,
{
id : bigint ({ mode : 'number' }). primaryKey (). generatedByDefaultAsIdentity ({
name : 'ai_credit_balance_id_seq' ,
startWith : 1 ,
increment : 1 ,
minValue : 1 ,
cache : 1
}),
orgId : uuid ( 'org_id' ). notNull (),
balance : integer (). notNull (). default ( 0 ),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . orgId ],
foreignColumns : [ organization . id ],
name : 'ai_credit_balance_org_id_fkey'
}),
unique ( 'ai_credit_balance_org_id_key' ). on ( table . orgId )
]
);
2026-05-09 06:05:03 +01:00
export const aiCreditPurchase = pgTable (
'ai_credit_purchase' ,
{
id : bigint ({ mode : 'number' }). primaryKey (). generatedByDefaultAsIdentity ({
name : 'ai_credit_purchase_id_seq' ,
startWith : 1 ,
increment : 1 ,
minValue : 1 ,
cache : 1
}),
orgId : uuid ( 'org_id' ). notNull (),
triggeredBy : uuid ( 'triggered_by' ),
provider : text (). notNull (). default ( 'polar' ),
providerOrderId : text ( 'provider_order_id' ). notNull (),
tokens : integer (). notNull (),
quantity : integer (). notNull (). default ( 1 ),
unitPriceCents : integer ( 'unit_price_cents' ). notNull (). default ( 0 ),
currency : text (). notNull (). default ( 'USD' ),
payload : jsonb (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . orgId ],
foreignColumns : [ organization . id ],
name : 'ai_credit_purchase_org_id_fkey'
}),
foreignKey ({
columns : [ table . triggeredBy ],
foreignColumns : [ profile . id ],
name : 'ai_credit_purchase_triggered_by_fkey'
}),
unique ( 'ai_credit_purchase_provider_order_id_key' ). on ( table . providerOrderId ),
index ( 'idx_ai_credit_purchase_org_created' ). on ( table . orgId , table . createdAt )
]
);
2026-05-18 03:47:22 +01:00
// ─── AI Tutor Fair-Use ───────────────────────────────────────────────────────
export const aiTutorMessageCount = pgTable (
'ai_tutor_message_count' ,
{
id : bigint ({ mode : 'number' }). primaryKey (). generatedByDefaultAsIdentity ({
name : 'ai_tutor_message_count_id_seq' ,
startWith : 1 ,
increment : 1 ,
minValue : 1 ,
cache : 1
}),
orgId : uuid ( 'org_id' ). notNull (),
userId : uuid ( 'user_id' ). notNull (),
/** First day of the calendar month this counter covers. */
periodStart : date ( 'period_start' ). notNull (),
messageCount : integer ( 'message_count' ). notNull (). default ( 0 ),
lastMessageAt : timestamp ( 'last_message_at' , { withTimezone : true , mode : 'string' }),
capHitAt : timestamp ( 'cap_hit_at' , { withTimezone : true , mode : 'string' }),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . orgId ],
foreignColumns : [ organization . id ],
name : 'ai_tutor_message_count_org_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . userId ],
foreignColumns : [ profile . id ],
name : 'ai_tutor_message_count_user_id_fkey'
}). onDelete ( 'cascade' ),
unique ( 'ai_tutor_message_count_org_user_period_key' ). on ( table . orgId , table . userId , table . periodStart ),
index ( 'idx_ai_tutor_message_count_org_period' ). on ( table . orgId , table . periodStart ),
index ( 'idx_ai_tutor_message_count_user' ). on ( table . userId , table . periodStart )
]
);
export const aiTutorCapEvent = pgTable (
'ai_tutor_cap_event' ,
{
id : bigint ({ mode : 'number' }). primaryKey (). generatedByDefaultAsIdentity ({
name : 'ai_tutor_cap_event_id_seq' ,
startWith : 1 ,
increment : 1 ,
minValue : 1 ,
cache : 1
}),
orgId : uuid ( 'org_id' ). notNull (),
userId : uuid ( 'user_id' ). notNull (),
courseId : uuid ( 'course_id' ),
/** 'cap_reached' | 'pool_exhausted' | 'tutor_disabled' */
eventType : text ( 'event_type' ). notNull (),
occurredAt : timestamp ( 'occurred_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . orgId ],
foreignColumns : [ organization . id ],
name : 'ai_tutor_cap_event_org_id_fkey'
}). onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . userId ],
foreignColumns : [ profile . id ],
name : 'ai_tutor_cap_event_user_id_fkey'
}). onDelete ( 'cascade' ),
index ( 'idx_ai_tutor_cap_event_org_occurred' ). on ( table . orgId , table . occurredAt ),
index ( 'idx_ai_tutor_cap_event_user' ). on ( table . userId , table . occurredAt )
]
);
2026-04-27 11:40:38 +01:00
export const aiChatConversation = pgTable (
'ai_chat_conversation' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey (),
courseId : uuid ( 'course_id' ). notNull (),
userId : uuid ( 'user_id' ). notNull (),
title : text (). default ( 'New conversation' ),
messages : jsonb (). notNull (). default ([]),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . courseId ],
foreignColumns : [ course . id ],
name : 'ai_chat_conversation_course_id_fkey'
}),
index ( 'idx_ai_chat_conversation_course_user' ). on ( table . courseId , table . userId )
]
);
export const aiChatDocument = pgTable (
'ai_chat_document' ,
{
id : text (). primaryKey (),
conversationId : uuid ( 'conversation_id' ). notNull (),
courseId : uuid ( 'course_id' ). notNull (),
userId : uuid ( 'user_id' ). notNull (),
assetId : uuid ( 'asset_id' ),
fileName : text ( 'file_name' ). notNull (),
mimeType : text ( 'mime_type' ). notNull (),
text : text (). notNull (),
wordCount : integer ( 'word_count' ). notNull (). default ( 0 ),
pageCount : integer ( 'page_count' ),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . conversationId ],
foreignColumns : [ aiChatConversation . id ],
name : 'ai_chat_document_conversation_id_fkey'
}). onDelete ( 'cascade' ),
index ( 'idx_ai_chat_document_conversation' ). on ( table . conversationId )
]
);
2026-05-18 03:47:22 +01:00
// ─── BullMQ Job State (Postgres source of truth) ─────────────────────────────
// User-visible status for queued/long-running domain runs. BullMQ holds the
// operational job; these tables hold the entity the dashboard polls and
// resumes against. See prd/notification-system and the bullmq plan.
export const jobStatus = pgEnum ( 'JOB_STATUS' , [ 'queued' , 'running' , 'completed' , 'failed' , 'canceled' ]);
/**
* `media_job` — one user-facing media post-processing run (per uploaded asset).
* The BullMQ FlowProducer orchestrates one or more attempts; this row stores
* the latest status the dashboard polls.
*/
export const mediaJob = pgTable (
'media_job' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' ). notNull (),
assetId : uuid ( 'asset_id' ). notNull (),
/** Snapshot of who started this run; never re-derived inside the worker. */
triggeredByProfileId : uuid ( 'triggered_by_profile_id' ),
status : jobStatus (). default ( 'queued' ). notNull (),
stage : varchar ({ length : 64 }),
progressPercent : integer ( 'progress_percent' ). default ( 0 ),
/** Latest BullMQ root job id from the FlowProducer tree. Operational only. */
rootJobId : text ( 'root_job_id' ),
/** All BullMQ job ids for this run, keyed by job name (operational). */
jobIds : jsonb ( 'job_ids' ). default ({}). $type < Record < string , string >>(),
attempt : integer (). default ( 0 ). notNull (),
maxAttempts : integer ( 'max_attempts' ). default ( 3 ). notNull (),
cancelRequestedAt : timestamp ( 'cancel_requested_at' , { withTimezone : true , mode : 'string' }),
/** Provider/cost data once we wire transcription billing. */
costCents : integer ( 'cost_cents' ). default ( 0 ),
result : jsonb (). $type < Record < string , unknown > | null >(),
error : jsonb (). $type < { code : string ; message : string ; details? : unknown } | null > (),
warnings : jsonb (). default ([]). $type < string [] >(),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . organizationId ],
foreignColumns : [ organization . id ],
name : 'media_job_organization_id_fkey'
})
. onUpdate ( 'cascade' )
. onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . assetId ],
foreignColumns : [ asset . id ],
name : 'media_job_asset_id_fkey'
})
. onUpdate ( 'cascade' )
. onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . triggeredByProfileId ],
foreignColumns : [ profile . id ],
name : 'media_job_triggered_by_profile_id_fkey'
}). onDelete ( 'set null' ),
index ( 'idx_media_job_asset_id' ). on ( table . assetId ),
index ( 'idx_media_job_org_status' ). on ( table . organizationId , table . status ),
index ( 'idx_media_job_updated_at' ). on ( table . updatedAt )
]
);
/**
* `job_step` — generic checkpoint ledger for any queued domain run. Workers
* look up `(runId, stepKey)` first and short-circuit if the step has a result,
* which makes BullMQ retries safe against partial completion.
*/
export const jobStep = pgTable (
'job_step' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
/** The owning domain (e.g. `media`, `course-import`, `onboarding-bootstrap`). */
domain : varchar ({ length : 64 }). notNull (),
/** Domain run id (e.g. `media_job.id`). */
runId : uuid ( 'run_id' ). notNull (),
/** Stable key for this step (e.g. `probe-metadata`). */
stepKey : varchar ( 'step_key' , { length : 96 }). notNull (),
status : jobStatus (). default ( 'queued' ). notNull (),
/** Hash of inputs to detect changed inputs across retries. */
inputHash : text ( 'input_hash' ),
/** Provider id (whisper job id, ffmpeg run id, etc.) for de-dup. */
providerId : text ( 'provider_id' ),
result : jsonb (). $type < Record < string , unknown > | null >(),
error : jsonb (). $type < { code : string ; message : string ; details? : unknown } | null > (),
attempt : integer (). default ( 0 ). notNull (),
startedAt : timestamp ( 'started_at' , { withTimezone : true , mode : 'string' }),
finishedAt : timestamp ( 'finished_at' , { withTimezone : true , mode : 'string' }),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
unique ( 'job_step_run_step_unique' ). on ( table . domain , table . runId , table . stepKey ),
index ( 'idx_job_step_run' ). on ( table . domain , table . runId )
]
);
/**
* Whisper-derived transcript + captions artifact for an uploaded video asset.
* One row per asset (re-transcribe replaces via upsert on asset_id).
*/
export const mediaTranscript = pgTable (
'media_transcript' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' ). notNull (),
assetId : uuid ( 'asset_id' ). notNull (),
mediaJobId : uuid ( 'media_job_id' ),
language : varchar ({ length : 8 }). notNull (),
provider : varchar ({ length : 32 }). default ( 'openai' ). notNull (),
model : varchar ({ length : 64 }). default ( 'whisper-1' ). notNull (),
text : text (). notNull (),
segments : jsonb (). $type < { start : number ; end : number ; text : string }[] > (). notNull (),
vttStorageKey : text ( 'vtt_storage_key' ). notNull (),
vttBucket : text ( 'vtt_bucket' ). notNull (),
durationSeconds : integer ( 'duration_seconds' ),
costCents : integer ( 'cost_cents' ). default ( 0 ). notNull (),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull (),
updatedAt : timestamp ( 'updated_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
foreignKey ({
columns : [ table . organizationId ],
foreignColumns : [ organization . id ],
name : 'media_transcript_organization_id_fkey'
})
. onUpdate ( 'cascade' )
. onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . assetId ],
foreignColumns : [ asset . id ],
name : 'media_transcript_asset_id_fkey'
})
. onUpdate ( 'cascade' )
. onDelete ( 'cascade' ),
foreignKey ({
columns : [ table . mediaJobId ],
foreignColumns : [ mediaJob . id ],
name : 'media_transcript_media_job_id_fkey'
})
. onUpdate ( 'cascade' )
. onDelete ( 'set null' ),
unique ( 'media_transcript_asset_id_unique' ). on ( table . assetId ),
index ( 'idx_media_transcript_asset' ). on ( table . assetId )
]
);
/**
* `dead_letter_job` — terminal failure record after BullMQ exhausts attempts
* or a domain explicitly dead-letters a run. Drives the admin retry/replay UI
* and Tinybird/Slack alerting.
*/
export const deadLetterJob = pgTable (
'dead_letter_job' ,
{
id : uuid ()
. default ( sql `gen_random_uuid()` )
. primaryKey ()
. notNull (),
organizationId : uuid ( 'organization_id' ),
domain : varchar ({ length : 64 }). notNull (),
runId : uuid ( 'run_id' ),
queueName : varchar ( 'queue_name' , { length : 64 }). notNull (),
jobName : varchar ( 'job_name' , { length : 96 }). notNull (),
/** Original BullMQ job id. */
bullmqJobId : text ( 'bullmq_job_id' ),
payload : jsonb (). $type < Record < string , unknown >>(),
error : jsonb (). $type < { code : string ; message : string ; stack? : string } > (),
attempts : integer (). default ( 0 ). notNull (),
/** Optional pointer to a replay job once an operator retries. */
replayedAt : timestamp ( 'replayed_at' , { withTimezone : true , mode : 'string' }),
createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'string' }). defaultNow (). notNull ()
},
( table ) => [
index ( 'idx_dead_letter_job_domain_created' ). on ( table . domain , table . createdAt ),
index ( 'idx_dead_letter_job_org_created' ). on ( table . organizationId , table . createdAt )
]
);