mirror of
https://github.com/hanzoai/captable.git
synced 2026-10-16 14:21:29 +00:00
The cap table is served at /v1/captable, org-scoped from the caller's own bearer. This app kept its own Postgres copy of the same entities, so the two could disagree about who owns what — the reason to move rather than keep both. The tRPC procedures for share classes, equity plans, shares, options, SAFEs and stakeholders now call one client (src/server/captable-api.ts) whose normalizers read either spelling the surface uses and degrade to an honest empty value rather than throwing inside a render. Each procedure keeps the shape it already returned, so no component changed for the move. The bearer rides the encrypted next-auth JWT and is read server-side only; it is never put on `session`, which is served to the browser verbatim. Paperwork stays local — buckets, uploaders, e-sign, data rooms are this app's. Document.shareId/optionId/safeId/convertibleNoteId keep their columns and now hold the id Hanzo Cloud assigned, so they are no longer foreign keys. Creates answer no body, so the new row is found by the natural key each write already enforces unique: certificateId, grantId, publicId. The public REST routes for shares and stakeholders are removed. They were a second write path over the same data, they authenticate with an app-issued API key that carries no IAM principal to present upstream, and one of them declared the stakeholder path — shadowing the real stakeholder PATCH, so PATCH on a share never existed. api.hanzo.ai/v1/captable is the public cap-table API. Two surfaces are left honest rather than faked: equityPlan.update refuses, because the backend serves no update route for plans; company.getCompany stays local, because /v1/captable/company carries six fields and that read feeds a form needing fourteen.
599 lines
14 KiB
Plaintext
599 lines
14 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "rhel-openssl-1.0.x"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
relationMode = "prisma"
|
|
}
|
|
|
|
generator json {
|
|
provider = "prisma-json-types-generator"
|
|
}
|
|
|
|
generator enums {
|
|
provider = "node prisma/enum-generator.cjs"
|
|
}
|
|
|
|
// Necessary for Next auth
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String?
|
|
access_token String?
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String?
|
|
session_state String?
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String? @unique
|
|
password String?
|
|
emailVerified DateTime?
|
|
image String?
|
|
|
|
accounts Account[]
|
|
sessions Session[]
|
|
memberships Member[]
|
|
passkeys Passkey[]
|
|
verificationToken VerificationToken[]
|
|
|
|
lastSignedIn DateTime @default(now())
|
|
identityProvider String?
|
|
|
|
accessTokens AccessToken[]
|
|
}
|
|
|
|
enum CredentialDeviceTypeEnum {
|
|
SINGLE_DEVICE
|
|
MULTI_DEVICE
|
|
}
|
|
|
|
model Passkey {
|
|
id String @id @default(cuid())
|
|
name String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @default(now())
|
|
lastUsedAt DateTime?
|
|
credentialId Bytes
|
|
credentialPublicKey Bytes
|
|
counter BigInt
|
|
credentialDeviceType CredentialDeviceTypeEnum
|
|
credentialBackedUp Boolean
|
|
transports String[]
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model PasskeyVerificationToken {
|
|
id String @id @unique @default(cuid())
|
|
token String @unique
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model VerificationToken {
|
|
id Int @id @default(autoincrement())
|
|
secondaryId String @unique @default(cuid())
|
|
identifier String
|
|
token String @unique
|
|
expires DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([identifier, token])
|
|
@@index([userId])
|
|
}
|
|
|
|
model PasswordResetToken {
|
|
id String @id @default(cuid())
|
|
email String
|
|
token String @unique
|
|
expires DateTime
|
|
|
|
@@unique([email, token])
|
|
}
|
|
|
|
model Company {
|
|
id String @id @default(cuid())
|
|
name String
|
|
logo String?
|
|
publicId String
|
|
website String?
|
|
incorporationType String
|
|
incorporationDate DateTime
|
|
incorporationCountry String
|
|
incorporationState String
|
|
|
|
streetAddress String
|
|
city String
|
|
state String
|
|
zipcode String
|
|
country String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
updates Update[]
|
|
users Member[]
|
|
audits Audit[]
|
|
documents Document[]
|
|
templates Template[]
|
|
dataRooms DataRoom[]
|
|
eSignAudits EsignAudit[]
|
|
customRoles CustomRole[]
|
|
|
|
BankAccount BankAccount[]
|
|
|
|
@@unique([publicId])
|
|
}
|
|
|
|
enum BankAccountTypeEnum {
|
|
CHECKING
|
|
SAVINGS
|
|
}
|
|
|
|
model BankAccount {
|
|
id String @id @default(cuid())
|
|
beneficiaryName String
|
|
beneficiaryAddress String
|
|
bankName String
|
|
bankAddress String
|
|
accountNumber String // iban or account number
|
|
routingNumber String // ABA or routing number
|
|
accountType BankAccountTypeEnum @default(CHECKING)
|
|
|
|
// International bank information
|
|
swiftCode String? // BIC or SWIFT code
|
|
|
|
primary Boolean @default(false)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([companyId, accountNumber])
|
|
@@unique([companyId, primary], name: "unique_primary_account")
|
|
@@index([companyId])
|
|
}
|
|
|
|
enum MemberStatusEnum {
|
|
ACTIVE
|
|
INACTIVE
|
|
PENDING
|
|
}
|
|
|
|
enum Roles {
|
|
ADMIN
|
|
CUSTOM
|
|
}
|
|
|
|
model Member {
|
|
id String @id @default(cuid())
|
|
title String?
|
|
status MemberStatusEnum @default(PENDING)
|
|
isOnboarded Boolean @default(false)
|
|
role Roles? @default(ADMIN)
|
|
workEmail String?
|
|
lastAccessed DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
|
|
customRoleId String?
|
|
customRole CustomRole? @relation(fields: [customRoleId], references: [id])
|
|
|
|
documentReceived EsignRecipient[]
|
|
documents Document[]
|
|
templates Template[]
|
|
updates Update[]
|
|
dataRooms DataRoomRecipient[]
|
|
UpdateRecipient UpdateRecipient[]
|
|
|
|
@@unique([companyId, userId])
|
|
@@index([companyId])
|
|
@@index([status])
|
|
@@index([userId])
|
|
@@index([customRoleId])
|
|
}
|
|
|
|
model CustomRole {
|
|
id String @id @default(cuid())
|
|
name String
|
|
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id])
|
|
|
|
permissions Json[]
|
|
member Member[]
|
|
|
|
@@index([companyId])
|
|
}
|
|
|
|
model Audit {
|
|
id String @id @default(cuid())
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
summary String?
|
|
action String
|
|
occurredAt DateTime @default(now())
|
|
actor Json
|
|
target Json[]
|
|
context Json
|
|
|
|
@@index([companyId])
|
|
}
|
|
|
|
model Bucket {
|
|
id String @id @default(cuid())
|
|
name String
|
|
key String
|
|
mimeType String
|
|
size Int
|
|
tags String[]
|
|
|
|
documents Document[]
|
|
templates Template[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Document {
|
|
id String @id @default(cuid())
|
|
publicId String
|
|
name String
|
|
|
|
bucketId String
|
|
bucket Bucket @relation(fields: [bucketId], references: [id], onDelete: Cascade)
|
|
|
|
uploaderId String?
|
|
uploader Member? @relation(fields: [uploaderId], references: [id], onDelete: Cascade)
|
|
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
|
|
// The security these describe lives in Hanzo Cloud (/v1/captable), so each of
|
|
// these holds the id it assigned rather than referencing a local row.
|
|
shareId String?
|
|
optionId String?
|
|
safeId String?
|
|
convertibleNoteId String?
|
|
|
|
documentShares DocumentShare[]
|
|
dataRooms DataRoomDocument[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([publicId])
|
|
@@index([bucketId])
|
|
@@index([uploaderId])
|
|
@@index([companyId])
|
|
@@index([shareId])
|
|
@@index([optionId])
|
|
@@index([safeId])
|
|
@@index([convertibleNoteId])
|
|
}
|
|
|
|
model DataRoom {
|
|
id String @id @default(cuid())
|
|
name String
|
|
publicId String
|
|
public Boolean @default(false)
|
|
documents DataRoomDocument[]
|
|
recipients DataRoomRecipient[]
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([publicId])
|
|
@@unique([companyId, name])
|
|
@@index([publicId])
|
|
@@index([companyId])
|
|
}
|
|
|
|
model DataRoomDocument {
|
|
id String @id @default(cuid())
|
|
dataRoomId String
|
|
documentId String
|
|
dataRoom DataRoom @relation(fields: [dataRoomId], references: [id], onDelete: Cascade)
|
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([dataRoomId, documentId])
|
|
@@index([dataRoomId])
|
|
@@index([documentId])
|
|
}
|
|
|
|
model DataRoomRecipient {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String
|
|
dataRoomId String
|
|
dataRoom DataRoom @relation(fields: [dataRoomId], references: [id], onDelete: Cascade)
|
|
|
|
memberId String?
|
|
member Member? @relation(fields: [memberId], references: [id], onDelete: SetNull)
|
|
|
|
// A cap-table stakeholder id from Hanzo Cloud, not a local row.
|
|
stakeholderId String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([dataRoomId, email])
|
|
@@index([id, dataRoomId])
|
|
@@index([memberId])
|
|
@@index([dataRoomId])
|
|
@@index([stakeholderId])
|
|
}
|
|
|
|
model UpdateRecipient {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String
|
|
updateId String
|
|
update Update @relation(fields: [updateId], references: [id], onDelete: Cascade)
|
|
|
|
memberId String?
|
|
member Member? @relation(fields: [memberId], references: [id], onDelete: SetNull)
|
|
|
|
// A cap-table stakeholder id from Hanzo Cloud, not a local row.
|
|
stakeholderId String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([updateId, email])
|
|
@@index([id, updateId])
|
|
@@index([memberId])
|
|
@@index([updateId])
|
|
@@index([stakeholderId])
|
|
}
|
|
|
|
model DocumentShare {
|
|
id String @id @default(cuid())
|
|
link String
|
|
publicId String
|
|
|
|
linkExpiresAt DateTime
|
|
recipients String[] @default([])
|
|
|
|
emailProtected Boolean @default(false)
|
|
|
|
documentId String
|
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([documentId])
|
|
}
|
|
|
|
enum FieldTypes {
|
|
TEXT
|
|
RADIO
|
|
EMAIL
|
|
DATE
|
|
DATETIME
|
|
TEXTAREA
|
|
CHECKBOX
|
|
SIGNATURE
|
|
SELECT
|
|
}
|
|
|
|
model TemplateField {
|
|
id String @id @default(cuid())
|
|
name String
|
|
type FieldTypes @default(TEXT)
|
|
defaultValue String @default("")
|
|
readOnly Boolean @default(false)
|
|
required Boolean @default(false)
|
|
prefilledValue String?
|
|
top Int
|
|
left Int
|
|
width Int
|
|
height Int
|
|
recipientId String
|
|
recipient EsignRecipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
|
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
templateId String
|
|
viewportHeight Int
|
|
viewportWidth Int
|
|
page Int
|
|
|
|
/// [TemplateFieldMeta]
|
|
meta Json @default("{}")
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([templateId])
|
|
@@index([recipientId])
|
|
}
|
|
|
|
enum TemplateStatus {
|
|
DRAFT
|
|
COMPLETE
|
|
PENDING
|
|
CANCELLED
|
|
}
|
|
|
|
model Template {
|
|
id String @id @default(cuid())
|
|
publicId String
|
|
name String
|
|
status TemplateStatus @default(DRAFT)
|
|
orderedDelivery Boolean @default(false)
|
|
message String?
|
|
|
|
bucketId String
|
|
bucket Bucket @relation(fields: [bucketId], references: [id], onDelete: Cascade)
|
|
|
|
uploaderId String
|
|
uploader Member @relation(fields: [uploaderId], references: [id], onDelete: Cascade)
|
|
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
fields TemplateField[]
|
|
eSignRecipient EsignRecipient[]
|
|
|
|
completedOn DateTime?
|
|
eSignAudits EsignAudit[]
|
|
|
|
@@index([bucketId])
|
|
@@index([uploaderId])
|
|
@@index([companyId])
|
|
}
|
|
|
|
enum EsignRecipientStatus {
|
|
SENT
|
|
SIGNED
|
|
PENDING
|
|
}
|
|
|
|
model EsignRecipient {
|
|
id String @id @default(cuid())
|
|
email String
|
|
name String?
|
|
|
|
templateId String
|
|
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
|
|
status EsignRecipientStatus @default(PENDING)
|
|
|
|
memberId String?
|
|
member Member? @relation(fields: [memberId], references: [id], onDelete: SetNull)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
templateFields TemplateField[]
|
|
eSignAudits EsignAudit[]
|
|
|
|
@@index([memberId])
|
|
@@index([templateId])
|
|
}
|
|
|
|
enum UpdateStatusEnum {
|
|
DRAFT
|
|
PUBLIC
|
|
PRIVATE
|
|
}
|
|
|
|
model Update {
|
|
id String @id @default(cuid())
|
|
publicId String
|
|
title String
|
|
content Json
|
|
html String
|
|
public Boolean @default(false)
|
|
status UpdateStatusEnum @default(DRAFT)
|
|
|
|
authorId String
|
|
companyId String
|
|
|
|
recipients UpdateRecipient[]
|
|
author Member @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([publicId])
|
|
@@index([publicId])
|
|
@@index([authorId])
|
|
@@index([companyId])
|
|
}
|
|
|
|
model EsignAudit {
|
|
id String @id @default(cuid())
|
|
|
|
companyId String
|
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
templateId String
|
|
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
recipientId String?
|
|
recipient EsignRecipient? @relation(fields: [recipientId], references: [id], onDelete: SetNull)
|
|
|
|
action String
|
|
ip String
|
|
userAgent String
|
|
location String
|
|
summary String
|
|
|
|
occurredAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([companyId])
|
|
@@index([templateId])
|
|
@@index([recipientId])
|
|
}
|
|
|
|
enum AccessTokenType {
|
|
sig @map("Signature token")
|
|
doc @map("Data room token")
|
|
api @map("API access token")
|
|
upd @map("Investor update token")
|
|
}
|
|
|
|
model AccessToken {
|
|
id String @id @default(cuid())
|
|
active Boolean @default(true)
|
|
clientId String
|
|
clientSecret String // Hashed with scrypt
|
|
typeEnum AccessTokenType @default(api)
|
|
|
|
user User @relation(fields: [userId], references: [id])
|
|
userId String
|
|
|
|
expiresAt DateTime?
|
|
lastUsed DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([typeEnum, clientId])
|
|
}
|