Database
Instructions for setting up and using the database.
Prisma
This project uses Prisma as the database client. Prisma is an agnostic object-relational mapper (ORM) that can be used with various database engines, including Postgres, MySQL, and even NoSQL engines like MongoDB.
While Postgres is recommended, the choice of database is ultimately up to you.
Postgres
To get started, you need to install Postgres (PostgreSQL). If you already have it installed or have chosen another database connector, you can skip this section.
Windows
- Visit the PostgreSQL Download page.
- Download and install Postgres.
Alternatively, use Chocolatey
which is like Homebrew
for Windows.
MacOS
If you are using a Mac, the recommended installation method is Homebrew
.
brew install postgresql
Initial User
On Windows, an initial user is usually created automatically. However, on MacOS and Linux, you need to create it manually. First, log into Postgres as a superuser:
sudo -u postgres psql
Then, create the initial user:
CREATE USER postgres WITH PASSWORD 'password';
ALTER USER postgres WITH SUPERUSER;
Exit the Postgres prompt with:
\q
Connection String
Replace the connection string in the .env
file with your Postgres credentials. Default credentials (username/password) are postgres and password.
DATABASE_URL=postgresql://postgres:password@localhost:5432/achromatic_pro?schema=public
Migrations
First make sure that the dependencies are installed:
npm install
To create the database and apply the migration files, run the following command:
npx prisma migrate dev
Prisma Studio
Prisma Studio is a useful tool for viewing and managing data in your local database. It serves as a lightweight alternative to tools like pgAdmin or DBeaver.
Visit Prisma Studio for more information.
Core Models
The most important models are:
- Account
- Contact
- Invitation
- Organization
- Session
- User
Model Relationships
- Organization -> Users
- Organization -> Contacts
- Organization -> Invitations
- User -> Sessions
- User -> Accounts
The Contact
model is an example of an aggregate with many dependent models, illustrating a real-world scenario. In an e-commerce context, this could be renamed to Customer
or something similar.
Multitenancy
Multitenancy is implemented by separating data in the same database using an organizationId
, also commonly known as the tenantId
.
For example the API keys of an organization
model ApiKey {
id String @id(map: "PK_ApiKey") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
description String @db.VarChar(70)
hashedKey String @unique()
expiresAt DateTime?
lastUsedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
The organizationId
is on the session object, i.e.
const session = await auth();
session.user.organizationId;
Prisma Schema
Below is the full Prisma schema used in the solution with all enumerations and models.
generator client {
provider = "prisma-client-js"
previewFeatures = []
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ---- Enumerations ---- //
enum ActionType {
CREATE @map("create")
UPDATE @map("update")
DELETE @map("delete")
}
enum ActorType {
SYSTEM @map("system")
MEMBER @map("member")
API @map("api")
}
enum ContactRecord {
PERSON @map("person")
COMPANY @map("company")
}
enum ContactStage {
LEAD @map("lead")
QUALIFIED @map("qualified")
OPPORTUNITY @map("opportunity")
PROPOSAL @map("proposal")
IN_NEGOTIATION @map("inNegotiation")
LOST @map("lost")
WON @map("won")
}
enum ContactTaskStatus {
OPEN @map("open")
COMPLETED @map("completed")
}
enum DayOfWeek {
SUNDAY @map("sunday")
MONDAY @map("monday")
TUESDAY @map("tuesday")
WEDNESDAY @map("wednesday")
THURSDAY @map("thursday")
FRIDAY @map("friday")
SATURDAY @map("saturday")
}
enum FeedbackCategory {
SUGGESTION @map("suggestion")
PROBLEM @map("problem")
QUESTION @map("question")
}
enum InvitationStatus {
PENDING @map("pending")
ACCEPTED @map("accepted")
REVOKED @map("revoked")
}
enum Role {
MEMBER @map("member")
ADMIN @map("admin")
}
enum WebhookTrigger {
CONTACT_CREATED @map("contactCreated")
CONTACT_UPDATED @map("contactUpdated")
CONTACT_DELETED @map("contactDeleted")
}
// ---- Models ---- //
model Account {
id String @id(map: "PK_Account") @default(uuid()) @db.Uuid
userId String @db.Uuid
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@unique([provider, providerAccountId])
}
model ApiKey {
id String @id(map: "PK_ApiKey") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
description String @db.VarChar(70)
hashedKey String @unique()
expiresAt DateTime?
lastUsedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
model ChangeEmailRequest {
id String @id(map: "PK_ChangeEmailRequest") @default(uuid()) @db.Uuid
userId String @db.Uuid
email String
expires DateTime
valid Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId], map: "IX_ChangeEmailRequest_userId")
}
model Contact {
id String @id(map: "PK_Contact") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
record ContactRecord @default(PERSON)
image String? @db.VarChar(2048)
name String @db.VarChar(255)
email String? @db.VarChar(255)
address String? @db.VarChar(255)
phone String? @db.VarChar(32)
stage ContactStage @default(LEAD)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
activities ContactActivity[]
comments ContactComment[]
notes ContactNote[]
pageVisits ContactPageVisit[]
tags ContactTag[]
favorites Favorite[]
tasks ContactTask[]
@@index([organizationId], map: "IX_Contact_organizationId")
}
model ContactActivity {
id String @id(map: "PK_ContactActivity") @default(uuid()) @db.Uuid
contactId String @db.Uuid
actionType ActionType
actorId String @db.VarChar(255)
actorType ActorType
metadata Json?
occuredAt DateTime @default(now())
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([contactId], map: "IX_ContactActivity_contactId")
@@index([occuredAt], map: "IX_ContactActivity_occuredAt")
}
model ContactComment {
id String @id(map: "PK_ContactComment") @default(uuid()) @db.Uuid
contactId String @db.Uuid
userId String @db.Uuid
text String @db.VarChar(2000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([contactId], map: "IX_ContactComment_contactId")
@@index([userId], map: "IX_ContactComment_userId")
}
model ContactImage {
id String @id(map: "PK_ContactImage") @default(uuid()) @db.Uuid
contactId String @db.Uuid
data Bytes?
contentType String? @db.VarChar(255)
hash String? @db.VarChar(64)
@@index([contactId], map: "IX_ContactImage_userId")
}
model ContactNote {
id String @id(map: "PK_ContactNote") @default(uuid()) @db.Uuid
contactId String @db.Uuid
userId String @db.Uuid
text String? @db.VarChar(8000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([contactId], map: "IX_ContactNote_contactId")
@@index([userId], map: "IX_ContactNote_userId")
}
model ContactPageVisit {
id String @id(map: "PK_ContactPageVisit") @default(uuid()) @db.Uuid
contactId String @db.Uuid
userId String? @db.Uuid
timestamp DateTime @default(now())
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
@@index([contactId], map: "IX_ContactPageVisit_contactId")
@@index([userId], map: "IX_ContactPageVisit_userId")
}
model ContactTag {
id String @id(map: "PK_ContactTag") @default(uuid()) @db.Uuid
text String @unique @db.VarChar(128)
contacts Contact[]
}
model ContactTask {
id String @id(map: "PK_ContactTask") @default(uuid()) @db.Uuid
contactId String @db.Uuid
title String @db.VarChar(255)
description String? @db.VarChar(8000)
status ContactTaskStatus @default(OPEN)
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([contactId], map: "IX_ContactTask_contactId")
}
model Favorite {
id String @id(map: "PK_Favorite") @default(uuid()) @db.Uuid
userId String @db.Uuid
contactId String @db.Uuid
order Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId], map: "IX_Favorite_userId")
@@index([contactId], map: "IX_Favorite_contactId")
}
model Feedback {
id String @id(map: "PK_Feedback") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
userId String? @db.Uuid
category FeedbackCategory @default(SUGGESTION)
message String @db.VarChar(4000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
@@index([organizationId], map: "IX_Feedback_organizationId")
@@index([userId], map: "IX_Feedback_userId")
}
model Invitation {
id String @id(map: "PK_Invitation") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
token String @default(uuid()) @db.Uuid
email String @db.VarChar(255)
role Role @default(MEMBER)
status InvitationStatus @default(PENDING)
lastSentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organiation Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([organizationId], map: "IX_Invitation_organizationId")
@@index([token], map: "IX_Invitation_token")
}
model Notification {
id String @id(map: "PK_Notification") @default(uuid()) @db.Uuid
userId String @db.Uuid
subject String? @db.VarChar(128)
content String @db.VarChar(8000)
link String? @db.VarChar(2000)
seenAt DateTime?
dismissed Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId], map: "IX_Notification_userId")
}
model Organization {
id String @id(map: "PK_Organization") @default(uuid()) @db.Uuid
stripeCustomerId String
name String @db.VarChar(255)
address String? @db.VarChar(255)
phone String? @db.VarChar(32)
email String? @db.VarChar(255)
website String? @db.VarChar(2000)
completedOnboarding Boolean @default(false)
billingPlan String @default("free") @db.VarChar(255)
apiKeys ApiKey[]
businessHours WorkHours[]
contacts Contact[]
feedback Feedback[]
invitations Invitation[]
users User[]
webhooks Webhook[]
}
model ResetPasswordRequest {
id String @id(map: "PK_ResetPasswordRequest") @default(uuid()) @db.Uuid
email String
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Session {
id String @id(map: "PK_Session") @default(uuid()) @db.Uuid
sessionToken String @unique
userId String @db.Uuid
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id String @id(map: "PK_User") @default(uuid()) @db.Uuid
organizationId String? @db.Uuid
image String? @db.VarChar(2048)
name String @db.VarChar(64)
email String? @unique
emailVerified DateTime?
password String? @db.VarChar(60)
lastLogin DateTime?
role Role @default(MEMBER)
phone String? @db.VarChar(32)
locale String @default("en-US") @db.VarChar(8)
completedOnboarding Boolean @default(false)
enabledContactsNotifications Boolean @default(false)
enabledInboxNotifications Boolean @default(false)
enabledWeeklySummary Boolean @default(false)
enabledNewsletter Boolean @default(false)
enabledProductUpdates Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
changeEmailRequests ChangeEmailRequest[]
comments ContactComment[]
favorites Favorite[]
feedback Feedback[]
notes ContactNote[]
notifications Notification[]
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
pageVisits ContactPageVisit[]
sessions Session[]
@@index([organizationId], map: "IX_User_organzationId")
}
model UserImage {
id String @id(map: "PK_UserImage") @default(uuid()) @db.Uuid
userId String @db.Uuid
data Bytes?
contentType String? @db.VarChar(255)
hash String? @db.VarChar(64)
@@index([userId], map: "IX_UserImage_userId")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Webhook {
id String @id(map: "PK_Webhook") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
url String @db.VarChar(2000)
triggers WebhookTrigger[]
secret String? @db.VarChar(1024)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
@@index([organizationId], map: "IX_Webhook_organizationId")
}
model WorkHours {
id String @id(map: "PK_WorkHours") @default(uuid()) @db.Uuid
organizationId String @db.Uuid
dayOfWeek DayOfWeek @default(SUNDAY)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
timeSlots WorkTimeSlot[]
@@index([organizationId], map: "IX_WorkHours_organizationId")
}
model WorkTimeSlot {
id String @id(map: "PK_WorkTimeSlot") @default(uuid()) @db.Uuid
workHoursId String @db.Uuid
start DateTime @db.Time(0)
end DateTime @db.Time(0)
workHours WorkHours @relation(fields: [workHoursId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([workHoursId], map: "IX_WorkTimeSlot_workHoursId")
}