Files
events-hub/backend/prisma/schema.prisma
joshuaandClaude Opus 5.5 e72330893a Hub v1: install API, licences, health dashboard, alerts
- Protocol 1 (docs/hub-protocol.md): public key, register, signed heartbeats
- Ed25519 licence tokens (30-day validity, status, expiry, grace, limits)
- Dashboard: installs, install detail, customers and licences, alerts,
  settings and audit log; two-factor login required
- Alert checks every 5 minutes, emailed once; heartbeat roll-up after 30 days
- Unit tests for encryption, tokens, signatures, TOTP and alert rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
2026-10-26 12:12:51 +02:00

150 lines
4.3 KiB
Plaintext

// Hope Events Hub — customers, licences, installs and their heartbeats.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
// Someone who logs in to the Hub (Joshua, later support staff). Two-factor (authenticator
// app) is required: totpSecret is set up on first login.
model AdminUser {
id String @id @default(uuid())
email String @unique
name String
password String
// AES-GCM encrypted (utils/crypto.js); null until two-factor is set up.
totpSecret String?
totpEnabled Boolean @default(false)
// Bumped to sign out everywhere.
tokenVersion Int @default(0)
lastLoginAt DateTime?
createdAt DateTime @default(now())
auditLogs AuditLog[]
}
model Customer {
id String @id @default(uuid())
name String
contactName String?
email String?
phone String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
licences Licence[]
installs Install[]
}
enum LicenceStatus {
active
suspended
revoked
}
model Licence {
id String @id @default(uuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id], onDelete: Restrict)
// CEH-XXXX-XXXX-XXXX-XXXX, generated by the Hub.
key String @unique
plan String @default("standard")
status LicenceStatus @default(active)
// Null = no end date.
expiresAt DateTime?
graceDays Int @default(14)
// Hope Events 2.0: monthly AI spend cap (in cents, rand). 0 = AI off.
aiMonthlyCapCents Int @default(0)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// One licence = one install.
install Install?
}
model Install {
id String @id @default(uuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id], onDelete: Restrict)
licenceId String? @unique
licence Licence? @relation(fields: [licenceId], references: [id], onDelete: SetNull)
name String
url String
// The install's HMAC secret, AES-GCM encrypted: the Hub needs it to check signatures.
secret String
version String?
protocolVersion Int @default(1)
adminContact Json?
server Json?
timezone String?
features Json?
// From the latest heartbeat.
lastHeartbeatAt DateTime?
health Json?
stats Json?
notes String?
registeredAt DateTime @default(now())
// Set when Joshua releases the licence (e.g. the church moved domain): heartbeats are refused.
releasedAt DateTime?
heartbeats Heartbeat[]
events InstallEvent[]
@@index([lastHeartbeatAt])
}
// Every heartbeat for 30 days, then one per install per day.
model Heartbeat {
id String @id @default(uuid())
installId String
install Install @relation(fields: [installId], references: [id], onDelete: Cascade)
receivedAt DateTime @default(now())
version String?
health Json?
stats Json?
@@index([installId, receivedAt])
}
enum EventLevel {
info
warning
critical
}
// Things that happened to an install: alerts (offline, backup failing, disk low, errors,
// licence expiring), version changes, registration. An alert stays open (resolvedAt null)
// until the condition clears, so it's emailed once.
model InstallEvent {
id String @id @default(uuid())
installId String
install Install @relation(fields: [installId], references: [id], onDelete: Cascade)
type String
level EventLevel @default(info)
message String
createdAt DateTime @default(now())
resolvedAt DateTime?
@@index([installId, createdAt])
@@index([type, resolvedAt])
}
model AuditLog {
id String @id @default(uuid())
actorId String?
actor AdminUser? @relation(fields: [actorId], references: [id], onDelete: SetNull)
action String
target String?
metadata Json?
ip String?
createdAt DateTime @default(now())
@@index([createdAt])
}
model Setting {
key String @id
value String
}