Prepare Portainer deployment

This commit is contained in:
Zachariah K. Sharma
2026-06-05 23:46:10 -06:00
commit e6f1c5c13f
84 changed files with 12383 additions and 0 deletions
@@ -0,0 +1,319 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"role" TEXT NOT NULL DEFAULT 'member',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "Form" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"ownerId" TEXT NOT NULL,
"fields" TEXT NOT NULL DEFAULT '[]',
"settings" TEXT NOT NULL DEFAULT '{}',
"published" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FormVersion" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"fields" TEXT NOT NULL,
"settings" TEXT NOT NULL,
"authorId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "FormVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FormViewer" (
"formId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "FormViewer_pkey" PRIMARY KEY ("formId","userId")
);
-- CreateTable
CREATE TABLE "Response" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"submitterId" TEXT,
"data" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Response_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FormEvent" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"sessionKey" TEXT NOT NULL,
"durationMs" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "FormEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PartialResponse" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"submitterId" TEXT,
"anonKey" TEXT,
"data" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PartialResponse_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UploadedFile" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"responseId" TEXT,
"uploaderId" TEXT,
"storageKey" TEXT NOT NULL,
"driver" TEXT NOT NULL DEFAULT 'local',
"name" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"contentType" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UploadedFile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WebhookDelivery" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"responseId" TEXT NOT NULL,
"url" TEXT NOT NULL DEFAULT '',
"status" INTEGER NOT NULL DEFAULT 0,
"attempts" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"nextAttemptAt" TIMESTAMP(3),
"lastAttemptAt" TIMESTAMP(3),
"payload" TEXT NOT NULL DEFAULT '',
"secret" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WebhookDelivery_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AccessToken" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"prefix" TEXT NOT NULL,
"lastUsedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AccessToken_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"color" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FormTag" (
"formId" TEXT NOT NULL,
"tagId" TEXT NOT NULL,
CONSTRAINT "FormTag_pkey" PRIMARY KEY ("formId","tagId")
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"actorId" TEXT,
"action" TEXT NOT NULL,
"entityType" TEXT NOT NULL,
"entityId" TEXT,
"metadata" TEXT NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "Form_slug_key" ON "Form"("slug");
-- CreateIndex
CREATE INDEX "FormVersion_formId_createdAt_idx" ON "FormVersion"("formId", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "FormVersion_formId_version_key" ON "FormVersion"("formId", "version");
-- CreateIndex
CREATE INDEX "FormEvent_formId_createdAt_idx" ON "FormEvent"("formId", "createdAt");
-- CreateIndex
CREATE INDEX "FormEvent_formId_kind_idx" ON "FormEvent"("formId", "kind");
-- CreateIndex
CREATE UNIQUE INDEX "PartialResponse_formId_submitterId_key" ON "PartialResponse"("formId", "submitterId");
-- CreateIndex
CREATE UNIQUE INDEX "PartialResponse_formId_anonKey_key" ON "PartialResponse"("formId", "anonKey");
-- CreateIndex
CREATE INDEX "UploadedFile_formId_createdAt_idx" ON "UploadedFile"("formId", "createdAt");
-- CreateIndex
CREATE INDEX "WebhookDelivery_formId_createdAt_idx" ON "WebhookDelivery"("formId", "createdAt");
-- CreateIndex
CREATE INDEX "WebhookDelivery_nextAttemptAt_idx" ON "WebhookDelivery"("nextAttemptAt");
-- CreateIndex
CREATE UNIQUE INDEX "AccessToken_tokenHash_key" ON "AccessToken"("tokenHash");
-- CreateIndex
CREATE UNIQUE INDEX "Tag_slug_key" ON "Tag"("slug");
-- CreateIndex
CREATE INDEX "FormTag_tagId_idx" ON "FormTag"("tagId");
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
-- CreateIndex
CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId");
-- CreateIndex
CREATE INDEX "AuditLog_actorId_createdAt_idx" ON "AuditLog"("actorId", "createdAt");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Form" ADD CONSTRAINT "Form_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormVersion" ADD CONSTRAINT "FormVersion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormVersion" ADD CONSTRAINT "FormVersion_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormViewer" ADD CONSTRAINT "FormViewer_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormViewer" ADD CONSTRAINT "FormViewer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Response" ADD CONSTRAINT "Response_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Response" ADD CONSTRAINT "Response_submitterId_fkey" FOREIGN KEY ("submitterId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormEvent" ADD CONSTRAINT "FormEvent_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PartialResponse" ADD CONSTRAINT "PartialResponse_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PartialResponse" ADD CONSTRAINT "PartialResponse_submitterId_fkey" FOREIGN KEY ("submitterId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UploadedFile" ADD CONSTRAINT "UploadedFile_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UploadedFile" ADD CONSTRAINT "UploadedFile_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AccessToken" ADD CONSTRAINT "AccessToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormTag" ADD CONSTRAINT "FormTag_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FormTag" ADD CONSTRAINT "FormTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+267
View File
@@ -0,0 +1,267 @@
// One workspace, one team. No multi-tenancy.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ---------- Auth.js (Prisma adapter) ----------
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
role String @default("member") // "admin" | "member"
createdAt DateTime @default(now())
accounts Account[]
sessions Session[]
forms Form[] @relation("FormOwner")
responses Response[]
viewers FormViewer[]
tokens AccessToken[]
uploads UploadedFile[]
partials PartialResponse[]
auditLogs AuditLog[]
formVersions FormVersion[]
}
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])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
// ---------- Forms ----------
model Form {
id String @id @default(cuid())
slug String @unique
title String
description String?
ownerId String
owner User @relation("FormOwner", fields: [ownerId], references: [id], onDelete: Cascade)
// Field schema stored as JSON: Field[] (see src/lib/types.ts)
fields String @default("[]")
// Form-level settings JSON: { visibility, thanksMessage, theme }
settings String @default("{}")
published Boolean @default(false)
responses Response[]
viewers FormViewer[]
uploads UploadedFile[]
partials PartialResponse[]
events FormEvent[]
versions FormVersion[]
tags FormTag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Immutable snapshot of a form's schema for revert/diff. One row per save.
model FormVersion {
id String @id @default(cuid())
formId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
// 1-indexed, monotonically increasing per form.
version Int
title String
description String?
fields String
settings String
// The user who saved this version (may be null if author was deleted).
authorId String?
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@unique([formId, version])
@@index([formId, createdAt])
}
// Who, besides owner & admins, can view responses.
model FormViewer {
formId String
userId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([formId, userId])
}
model Response {
id String @id @default(cuid())
formId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
submitterId String?
submitter User? @relation(fields: [submitterId], references: [id], onDelete: SetNull)
// JSON: Record<fieldId, value>
data String
createdAt DateTime @default(now())
}
model FormEvent {
id String @id @default(cuid())
formId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
// "view" | "start" | "submit" | "abandon"
kind String
sessionKey String
durationMs Int?
createdAt DateTime @default(now())
@@index([formId, createdAt])
@@index([formId, kind])
}
model PartialResponse {
id String @id @default(cuid())
formId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
submitterId String?
submitter User? @relation(fields: [submitterId], references: [id], onDelete: SetNull)
anonKey String?
data String
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
// Nulls are distinct in Postgres, so multiple rows with one side null
// can coexist — that's intended (one row per (form, identity)).
@@unique([formId, submitterId])
@@unique([formId, anonKey])
}
model UploadedFile {
id String @id @default(cuid())
formId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
responseId String?
uploaderId String?
uploader User? @relation(fields: [uploaderId], references: [id], onDelete: SetNull)
// Storage key — opaque path/key in the configured driver.
storageKey String
// Storage driver that wrote this object (so we can read it back later
// even if the default is later switched).
driver String @default("local")
name String
size Int
contentType String
createdAt DateTime @default(now())
@@index([formId, createdAt])
}
// Outbound webhook delivery record. One row per attempt-chain; deliveries with
// remaining retries have status=0 and nextAttemptAt in the future.
model WebhookDelivery {
id String @id @default(cuid())
formId String
responseId String
url String @default("")
status Int @default(0)
attempts Int @default(0)
lastError String?
// Scheduled time of the next retry. When null, no more retries are pending.
nextAttemptAt DateTime?
lastAttemptAt DateTime?
// The signed JSON body, kept so a worker can retry without re-loading state.
payload String @default("")
// Secret used for HMAC, kept with the row for retry independence.
secret String?
createdAt DateTime @default(now())
@@index([formId, createdAt])
@@index([nextAttemptAt])
}
// Personal access tokens — used for the MCP endpoint.
model AccessToken {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String
// sha256 of the secret; secret is shown once.
tokenHash String @unique
prefix String // first 8 chars of the secret, shown in lists
lastUsedAt DateTime?
createdAt DateTime @default(now())
}
// ---------- Tags ----------
model Tag {
id String @id @default(cuid())
// Lowercase, unique. Display name preserves the casing of the first use.
slug String @unique
name String
// Optional accent color (hex) for the chip; falls back to theme default.
color String?
createdAt DateTime @default(now())
forms FormTag[]
}
model FormTag {
formId String
tagId String
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([formId, tagId])
@@index([tagId])
}
// ---------- Audit log ----------
// Append-only feed of mutations. Visible to admins.
model AuditLog {
id String @id @default(cuid())
// Who did the thing. Null only for system actions (cron, webhook worker, etc).
actorId String?
actor User? @relation(fields: [actorId], references: [id], onDelete: SetNull)
// Verb in past tense: "form.created", "form.deleted", "member.role_changed", etc.
action String
// What the action operated on.
entityType String
entityId String?
// Free-form JSON for action-specific details (old/new values, etc.).
metadata String @default("{}")
createdAt DateTime @default(now())
@@index([createdAt])
@@index([entityType, entityId])
@@index([actorId, createdAt])
}