Enhancing the flexibility of the auth schema #769
Replies: 5 comments 13 replies
-
Interesting conversation! I wonder how the SSO could be integrated into this equation where an organization would be given an option to mandate its users only logging in through a specific SAML or OIDC auth provider |
Beta Was this translation helpful? Give feedback.
-
I'll participate by sharing my current setup more in detail, in order to deliver something to the discussion and learn why this approach may not be viable. And maybe it's a simple step up for people who want to implement something like this. Since I have an App built for companies/agencies, I was going for a multi tenant setup. Many companies work with freelancers so in edge cases you would have this situation that a freelancer can work for both companies, and does need to have access to both workspaces. So:
model User {
id String @id @default(cuid())
email String @unique
username String @unique
firstName String?
lastName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
image UserImage?
password Password?
roles Role[]
sessions Session[]
connections Connection[]
authenticators Authenticator[]
currentWorkspaceId String?
workspaces UserWorkspace[]
novuSubscriberId NovuSubscriberID[]
} model UserWorkspace {
userId String
workspaceId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@id([userId, workspaceId])
} model Workspace {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
users UserWorkspace[]
invitation Invitation[]
// Stripe Customer in this case is the workspace
customerId String? @default("")
// Stripe Subscription
subscription Subscription?
} At every workspace related page, I check if:
Switching workspaces involves making a POST and updating the currentWorkspaceID export async function loader({ request }: LoaderFunctionArgs) {
const workspaceId = await requireUserWorkspaceId(request)
if (!workspaceId) {
return redirect('/create-workspace')
}
const totalContacts = await prisma.contact.count({
where: { workspaceId },
}) |
Beta Was this translation helpful? Give feedback.
-
Does it make sense to separate Organization, Workspace, and User? For example, it seems to be pretty common for an Organization to have separate internal teams with a workspace per project, and multiple users per workspace. |
Beta Was this translation helpful? Give feedback.
-
I made something similar here is the schema. model User {
id String @id @default(cuid())
email String @unique
username String @unique
name String?
// Optional Currently maybe add this later
inviteKey String
canCreateCompanies Boolean @default(false)
companiesLimit Int @default(3)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
password Password?
platformStatus PlatformStatus @relation(fields: [platformStatusKey], references: [key])
platformStatusKey String
roles Role[]
sessions Session[]
connections Connection[]
userCompanies UserCompany[]
companyInvitations CompanyMemberInvitation[]
@@unique([email, inviteKey])
}
model Permission {
id String @id @default(cuid())
action String // e.g. create, read, update, delete
entity String // e.g. note, user, etc.
access String // e.g. own or any
description String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
roles Role[]
userCompanies UserCompany[]
@@unique([action, entity, access])
}
model Role {
id String @id @default(cuid())
name String @unique
description String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
permissions Permission[]
userCompanies UserCompany[]
companyMemberInvitation CompanyMemberInvitation[]
}
model PlatformStatus {
id Int @id @default(autoincrement())
key String @unique
label String?
color String?
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
companies Company[]
users User[]
userCompanies UserCompany[]
}
// * Account specific models
// * This is for a simple personal finance app
// Company model represents a business entity or organization that the accounting system will handle.
model Company {
id String @id @default(cuid())
name String // Name of the company or business entity.
linkKey String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
platformStatus PlatformStatus @relation(fields: [platformStatusKey], references: [key])
platformStatusKey String
invoiceCount Int @default(0) // Count of invoices generated by the company.
users UserCompany[] // Users associated with the company.
accounts Account[] @relation("OriginCompany") // Financial accounts belonging to the company.
linkedAccounts Account[] @relation("LinkedPlatformCompanyAccount")
transactions Transaction[] // Financial transactions recorded for the company.
saleInvoices SaleInvoice[] // Invoices generated by the company.
saleItems SaleInvoiceItem[] // Bills received by the company.
purchaseBills PurchaseBill[]
inventory InventoryItem[]
requestInventory RequestInventory[]
cart Cart[]
memberInvitation CompanyMemberInvitation[]
@@unique([id, linkKey])
}
// UserCompany is a join table that allows users to be associated with multiple companies.
// It serves as a many-to-many relationship between User and Company models.
model UserCompany {
id String @id @default(cuid())
isOwner Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade, onUpdate: Cascade)
companyId String
cart Cart?
roles Role[]
permissions Permission[]
status PlatformStatus @relation(fields: [statusKey], references: [key])
statusKey String
transactions Transaction[]
saleInvoices SaleInvoice[]
purchaseBills PurchaseBill[]
accounts Account[]
inventory InventoryItem[]
requestInventory RequestInventory[]
@@unique([userId, companyId])
}
model CompanyMemberInvitation {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
email String
isAccepted Boolean @default(false)
status String // e.g. pending, accepted, rejected
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade, onUpdate: Cascade)
companyId String
role Role? @relation(fields: [roleId], references: [id])
roleId String?
user User? @relation(fields: [userEmail, userInviteKey], references: [email, inviteKey])
userEmail String?
userInviteKey String?
}
// model represents financial ledgers which are used to record transactions
// and keep track of the financial status of various aspects of the company such as assets, liabilities, revenue, etc.
model Account {
id String @id @default(cuid())
name String // Name of the financial ledger/account.
uniqueId String?
email String?
balance Float? // Current balance of the account.
// type String // Type of the account, e.g., revenue, expense.
phone String?
country String?
city String?
state String?
zip String?
address String?
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Get approval of both the companies
isLinkedCompanyAccount Boolean @default(false)
innitiatorApproval Boolean @default(false)
linkedCompanyApproval Boolean @default(false)
company Company @relation("OriginCompany", fields: [companyId], references: [id], onDelete: Cascade, onUpdate: Cascade)
companyId String
linkedCompany Company? @relation("LinkedPlatformCompanyAccount", fields: [linkedCompanyId, linkedCompanyKey], references: [id, linkKey], onDelete: Cascade, onUpdate: Cascade)
linkedCompanyId String?
linkedCompanyKey String?
createdBy UserCompany @relation(fields: [createdById], references: [id])
createdById String
transactions Transaction[] // Transactions associated with this account.
PurchaseBill PurchaseBill[] // Bills associated with this account.
SaleInvoice SaleInvoice[] // Invoices associated with this account.
} |
Beta Was this translation helpful? Give feedback.
-
The tentative schema I'm working with looks like this right now, but as I go through each use-case I might make some tweaks to handle it
model Organization {
id String @id @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships Membership[]
}
model Membership {
id String @id @default(uuid())
userId String?
organizationId String
invitedById String?
user User? @relation("MembershipUser", fields: [userId], references: [id])
invitedBy User? @relation("InvitedByUser", fields: [invitedById], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
roles Role[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([invitedById])
@@index([organizationId])
}
model User {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships Membership[] @relation("MembershipUser")
invitedMemberships Membership[] @relation("InvitedByUser")
account Account @relation(fields: [accountId], references: [id])
accountId String
// The following fields are app-specific and you might want to change them
name String
username String
email String
image UserImage?
notes Note[]
}
model Session {
id String @id @default(uuid())
expiresAt DateTime
accounts Account[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(uuid())
email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
password Password?
sessions Session[]
users User[]
connections Connection[]
}
model Password {
accountId String @unique
hash String
account Account @relation(fields: [accountId], references: [id])
}
model Connection {
id String @id @default(cuid())
providerName String
providerId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
account Account? @relation(fields: [accountId], references: [id])
accountId String?
@@unique([providerName, providerId])
}
model Permission {
id String @id @default(cuid())
action String
entity String
access String
description String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
roles Role[] @relation("RolePermissions")
@@unique([action, entity, access])
}
model Role {
id String @id @default(cuid())
name String @unique
description String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
permissions Permission[] @relation("RolePermissions")
memberships Membership[]
} |
Beta Was this translation helpful? Give feedback.
-
This was brought up by @jacobparis and discussed by myself, @sergiodxa, @glitchassassin, and @DennisKraaijeveld
Here's an AI summary of the conversation (which started here):
Summary of Discord Conversation:
Participants:
Key Points:
Error Boundary in Onboarding:
onboarding.tsx
is missing a general error boundary and plans to submit a PR to address this. He's working on a Remix Vercel project and is heavily copying the auth setup from Epic Stack, making necessary adjustments like switching Resend for SendGrid.Flexible Auth System:
Terminology and Structure:
Multi-Tenancy and Data Modeling:
Implementation Details:
Next Steps:
Let's continue this discussion on GitHub to further explore the implementation details and best practices for the flexible auth system and multi-tenancy data modeling.
Beta Was this translation helpful? Give feedback.
All reactions