Enhancing the flexibility of the auth schema #769
Replies: 5 comments 12 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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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