diff --git a/app/components/ChatInput.vue b/app/components/ChatInput.vue
index ed8c0c1..3b93f58 100644
--- a/app/components/ChatInput.vue
+++ b/app/components/ChatInput.vue
@@ -178,7 +178,12 @@ watch(() => props.agent?.defaultModelId, (newModelId) => {
}
})
+// sending needs a model, and there are none until a provider is set up
+const noModels = computed(() => allModels.value.length === 0);
+
const handleSubmit = () => {
+ if (noModels.value) return;
+
if (isUploading.value || hasFailedUploads.value) {
return;
}
@@ -454,8 +459,8 @@ onUnmounted(() => {
-
-
+
- {{ selectedModel ? selectedModel.name : 'Select a model' }}
+ {{ noModels ? 'No models yet' : selectedModel ? selectedModel.name : 'Select a model' }}
@@ -246,9 +251,21 @@ onUnmounted(() => {
? 'hidden'
: 'visible',
}"
- class="fixed z-100 w-full max-w-[420px] min-w-[280px] flex flex-col rounded-xl border border-[var(--color-border)] bg-[var(--bg-surface)] shadow-lg overflow-hidden"
- :class="transformOrigin">
+ :class="[transformOrigin, noModels ? 'max-w-80' : 'max-w-[420px]']"
+ class="fixed z-100 w-full min-w-[280px] flex flex-col rounded-xl border border-[var(--color-border)] bg-[var(--bg-surface)] shadow-lg overflow-hidden">
+
+
No models yet
+
+ Veridian uses your own API keys. Add a provider and its models show up here.
+
+
+ Add a provider
+
+
+
+
@@ -305,6 +322,7 @@ onUnmounted(() => {
+
diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts
index 820f11f..dff7869 100644
--- a/app/composables/useAuth.ts
+++ b/app/composables/useAuth.ts
@@ -162,7 +162,9 @@ export const useAuth = () => {
const signUp = async (
email: string,
password: string,
- name: string
+ name: string,
+ // needed while the instance only takes invited people
+ inviteToken?: string,
): Promise
> => {
sessionFetching.value = true;
@@ -171,6 +173,8 @@ export const useAuth = () => {
email,
password,
name,
+ }, {
+ headers: inviteToken ? { 'x-invite-token': inviteToken } : undefined,
});
if (error) {
diff --git a/app/pages/auth/register.vue b/app/pages/auth/register.vue
index fdb4eff..73f2470 100644
--- a/app/pages/auth/register.vue
+++ b/app/pages/auth/register.vue
@@ -12,13 +12,42 @@ if (session.value !== null) {
await navigateTo(to ?? '/');
}
+const DAY_MS = 24 * 60 * 60 * 1000;
+
// who may register is an instance setting, admins change it while it runs
const { data: instance } = await useFetch('/api/instance', { key: 'instance' });
-if (instance.value && instance.value.registration !== 'open') {
+if (instance.value?.registration === 'closed') {
await navigateTo(to ? `/auth/login?to=${to}` : '/auth/login');
}
+const inviteOnly = computed(() => instance.value?.registration === 'invite');
+
+// invite links look like /auth/register?invite=
+const inviteToken = typeof useRoute().query.invite === 'string' ? useRoute().query.invite as string : undefined;
+
+const { data: invite } = await useAsyncData(`invite_${inviteToken ?? 'none'}`, async () => {
+ if (inviteToken === undefined) return null;
+
+ return await $fetch(`/api/invite/${encodeURIComponent(inviteToken)}`);
+});
+
+// set when the invite turns out to be used up or expired on submit
+const inviteRejected = ref(false);
+
+const validInvite = computed(() => !inviteRejected.value && invite.value?.valid ? invite.value : null);
+
+// an invite only instance takes nobody without a working link
+const blocked = computed(() => inviteOnly.value && validInvite.value === null);
+
+const inviteValidFor = computed(() => {
+ if (!validInvite.value) return '';
+
+ const days = Math.max(1, Math.ceil((new Date(validInvite.value.expiresAt).getTime() - Date.now()) / DAY_MS));
+
+ return `Invite valid for ${days} more ${days === 1 ? 'day' : 'days'}${validInvite.value.singleUse ? ' · single use' : ''}`;
+});
+
const form = reactive({
name: '',
email: '',
@@ -56,6 +85,9 @@ onMounted(() => {
form.confirmPassword = tempForm.confirmPassword;
hydrated.value = true;
+ // there is no form when the invite does not work
+ if (blocked.value) return;
+
nameInputEl.value!.addEventListener('input', () => {
nameInputEl.value!.setCustomValidity('');
});
@@ -95,7 +127,7 @@ const submit = async () => {
loading.value = true;
- const res = await signUp(form.email, form.password, form.name);
+ const res = await signUp(form.email, form.password, form.name, inviteOnly.value ? inviteToken : undefined);
loading.value = false;
if (!res.ok) {
if (!res.error.data) {
@@ -122,6 +154,10 @@ const submit = async () => {
passwordInputEl.value!.setCustomValidity(res.error.data.message!);
passwordInputEl.value!.reportValidity();
break;
+ case 'INVITE_INVALID':
+ // used by someone else or expired while this page was open
+ inviteRejected.value = true;
+ break;
case 'REGISTRATION_CLOSED':
// an admin closed registration while this page was open
alert('Registration is closed on this instance. Ask an admin for an account.');
@@ -138,7 +174,8 @@ const submit = async () => {
const key = await deriveKey(form.password, res.data.user.id);
localStorage.setItem('encryptionKey', JSON.stringify(key));
- return navigateTo(to ?? '/');
+ // connecting a provider comes next, there is nothing to chat with before that
+ return navigateTo(to ? `/welcome?to=${to}` : '/welcome');
};
@@ -158,7 +195,30 @@ const submit = async () => {
Create your account
-
+
+
+ This instance is invite only. Ask an admin for an invite link to create an account.
+
+
+ This invite link doesn't work anymore. It was used already, has expired, or was revoked.
+ Ask an admin for a new one.
+
+
+
+
+
+
+
+ {{ validInvite.inviter.name }} invited you to this
+ instance
+
+ {{ inviteValidFor }}
+
+
+
+
diff --git a/app/pages/settings/instance/users.vue b/app/pages/settings/instance/users.vue
index fb40981..67e41f0 100644
--- a/app/pages/settings/instance/users.vue
+++ b/app/pages/settings/instance/users.vue
@@ -178,6 +178,49 @@ const deleteUser = (user: UserRow) => confirmThen({
return true;
}));
+// ---------------------------------------------------------------------------
+// Invite links
+// ---------------------------------------------------------------------------
+
+// only offered while the instance is invite only, links do nothing otherwise
+const { data: instance } = await useFetch<{ registration: string }>('/api/admin/instance', {
+ key: 'admin_instance',
+});
+
+const inviteBusy = ref(false);
+const inviteLink = ref('');
+const inviteCopied = ref(false);
+
+const createInvite = async () => {
+ if (inviteBusy.value) return;
+
+ inviteBusy.value = true;
+ actionError.value = '';
+ inviteCopied.value = false;
+
+ try {
+ const invite = await $fetch<{ token: string }>('/api/admin/invites', { method: 'POST' });
+ inviteLink.value = `${window.location.origin}/auth/register?invite=${invite.token}`;
+ // the Instance page lists the links, so it has to load the new one
+ clearNuxtData('admin_invites');
+ } catch (error) {
+ console.error('Failed to create invite:', error);
+ actionError.value = 'The invite link could not be created. Try again.';
+ inviteBusy.value = false;
+ return;
+ }
+
+ // copying can be refused by the browser, the link is shown either way
+ try {
+ await navigator.clipboard.writeText(inviteLink.value);
+ inviteCopied.value = true;
+ } catch (error) {
+ console.error('Failed to copy invite link:', error);
+ }
+
+ inviteBusy.value = false;
+};
+
const openCreateUser = () => {
openDialog(DialogType.CreateUser, async (result) => {
if (result.ok === false) return;
@@ -208,10 +251,29 @@ const openCreateUser = () => {
-
- Create user
-
+
+
+ Create user
+
+
+
+ Invite link
+
+
+
+
+
+
+ {{ inviteCopied ? 'Invite link copied:' : 'New invite link:' }}
+
+
+ {{ inviteLink }}
+
+ one use · expires in 7 days
The users could not be loaded.
diff --git a/app/pages/welcome.vue b/app/pages/welcome.vue
new file mode 100644
index 0000000..9e6176c
--- /dev/null
+++ b/app/pages/welcome.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Veridian
+
One more thing: connect an AI provider
+
+
+
+ Veridian runs on your own API keys. Add one provider now and its models are ready when you
+ land on Home.
+
+
+
+
+
+
diff --git a/drizzle/migrations/20261008184328_invites/migration.sql b/drizzle/migrations/20261008184328_invites/migration.sql
new file mode 100644
index 0000000..1fdae68
--- /dev/null
+++ b/drizzle/migrations/20261008184328_invites/migration.sql
@@ -0,0 +1,11 @@
+CREATE TABLE "invites" (
+ "id" text PRIMARY KEY,
+ "token" text NOT NULL UNIQUE,
+ "created_by" text NOT NULL,
+ "max_uses" integer DEFAULT 1 NOT NULL,
+ "uses" integer DEFAULT 0 NOT NULL,
+ "expires_at" timestamp with time zone NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "invites" ADD CONSTRAINT "invites_created_by_users_id_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE CASCADE;
\ No newline at end of file
diff --git a/drizzle/migrations/20261008184328_invites/snapshot.json b/drizzle/migrations/20261008184328_invites/snapshot.json
new file mode 100644
index 0000000..28558e9
--- /dev/null
+++ b/drizzle/migrations/20261008184328_invites/snapshot.json
@@ -0,0 +1,3229 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "bb38ae6d-7cdd-4da9-8b3c-49e7d7ec98f3",
+ "prevIds": [
+ "2a4edef1-7e2b-45fe-aa8e-f7c8b32de18d"
+ ],
+ "ddl": [
+ {
+ "values": [
+ "reasoning",
+ "text",
+ "tool-call",
+ "file"
+ ],
+ "name": "part_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "user",
+ "assistant"
+ ],
+ "name": "role",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "pending",
+ "completed",
+ "failed",
+ "cancelled"
+ ],
+ "name": "status",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "accounts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "agents",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "attachments",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "embeddings",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "files",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "generations",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instance_settings",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "invites",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "message_parts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "messages",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "models",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "providers",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "sessions",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "settings",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "tool_calls",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "topics",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "two_factors",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "users",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "verifications",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "account_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "provider_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id_token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "access_token_expires_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "refresh_token_expires_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "scope",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "password",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "system_prompt",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "image_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "default_model_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "config",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "topic_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "message_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "file_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "file_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "content",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "type": "vector(1536)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "embedding",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "metadata",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "mime_type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "size",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "topic_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "model_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "status",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "status",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokens",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "error",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_settings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'open'",
+ "generated": null,
+ "identity": null,
+ "name": "registration",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_settings"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_settings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created_by",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "1",
+ "generated": null,
+ "identity": null,
+ "name": "max_uses",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "uses",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "topic_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "message_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tool_call_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "part_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "content",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "provider_options",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "finished",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "last_updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "topic_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "parent_message_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "generation_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "role",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "role",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "content",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "active_child_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "deleted",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "external_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "provider_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "cost",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 1,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "input_modalities",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 1,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "output_modalities",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 1,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "capabilities",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "context_window",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 1,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "supported_parameters",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_custom",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "true",
+ "generated": null,
+ "identity": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "released_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "true",
+ "generated": null,
+ "identity": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "config",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "ip_address",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_agent",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "impersonated_by",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "settings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "settings"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "system_assistants",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "settings"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "appearance",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "settings"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tool_name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "status",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'pending'",
+ "generated": null,
+ "identity": null,
+ "name": "status",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "input",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "output",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "error",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "agent_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "renaming",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rename_attempt_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rename_expires_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'manual'",
+ "generated": null,
+ "identity": null,
+ "name": "title_source",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "secret",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "backup_codes",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "true",
+ "generated": null,
+ "identity": null,
+ "name": "verified",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "failed_verification_count",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "locked_until",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "email_verified",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "image",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "two_factor_enabled",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "'user'",
+ "generated": null,
+ "identity": null,
+ "name": "role",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "banned",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "ban_reason",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "ban_expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "users"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "identifier",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "value",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "now()",
+ "generated": null,
+ "identity": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "accounts_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "agents_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "topic_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "attachments_topicId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "attachments_messageId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "attachments_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "file_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "embeddings_fileId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "files_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "files"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "topic_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "generations_topicId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "generations_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "topic_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "message_parts_topicId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "message_parts_messageId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "message_parts_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "topic_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "messages_topicId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "parent_message_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "messages_parentMessageId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "messages_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "models_providerId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "models_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "providers_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "providers"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "sessions_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "settings_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "settings"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "tool_calls_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "tool_calls"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "agent_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "topics_agentId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "topics_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "secret",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "two_factors_secret_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "two_factors_userId_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "verifications_identifier_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "verifications"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "users",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "accounts_user_id_users_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "default_model_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "models",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "name": "agents_default_model_id_models_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "agents"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "topic_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "topics",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "attachments_topic_id_topics_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "message_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "messages",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "attachments_message_id_messages_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "file_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "files",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "attachments_file_id_files_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "attachments"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "file_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "files",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "embeddings_file_id_files_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "embeddings"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "topic_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "topics",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "generations_topic_id_topics_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "generations"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "created_by"
+ ],
+ "schemaTo": "public",
+ "tableTo": "users",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "invites_created_by_users_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "invites"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "topic_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "topics",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "message_parts_topic_id_topics_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "message_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "messages",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "message_parts_message_id_messages_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "tool_call_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "tool_calls",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "message_parts_tool_call_id_tool_calls_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "message_parts"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "topic_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "topics",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "messages_topic_id_topics_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "generation_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "generations",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "messages_generation_id_generations_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "messages"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "provider_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "providers",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "models_provider_id_providers_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "models"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "users",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "sessions_user_id_users_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "agent_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "agents",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "topics_agent_id_agents_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "topics"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "users",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "two_factors_user_id_users_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "two_factors"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "accounts_pkey",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "agents_pkey",
+ "schema": "public",
+ "table": "agents",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "attachments_pkey",
+ "schema": "public",
+ "table": "attachments",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "embeddings_pkey",
+ "schema": "public",
+ "table": "embeddings",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "files_pkey",
+ "schema": "public",
+ "table": "files",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "generations_pkey",
+ "schema": "public",
+ "table": "generations",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "instance_settings_pkey",
+ "schema": "public",
+ "table": "instance_settings",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "invites_pkey",
+ "schema": "public",
+ "table": "invites",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "message_parts_pkey",
+ "schema": "public",
+ "table": "message_parts",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "messages_pkey",
+ "schema": "public",
+ "table": "messages",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "models_pkey",
+ "schema": "public",
+ "table": "models",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "providers_pkey",
+ "schema": "public",
+ "table": "providers",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "sessions_pkey",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "settings_pkey",
+ "schema": "public",
+ "table": "settings",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "tool_calls_pkey",
+ "schema": "public",
+ "table": "tool_calls",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "topics_pkey",
+ "schema": "public",
+ "table": "topics",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "two_factors_pkey",
+ "schema": "public",
+ "table": "two_factors",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "users_pkey",
+ "schema": "public",
+ "table": "users",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "verifications_pkey",
+ "schema": "public",
+ "table": "verifications",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "token"
+ ],
+ "nullsNotDistinct": false,
+ "name": "invites_token_key",
+ "schema": "public",
+ "table": "invites",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "token"
+ ],
+ "nullsNotDistinct": false,
+ "name": "sessions_token_key",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "email"
+ ],
+ "nullsNotDistinct": false,
+ "name": "users_email_key",
+ "schema": "public",
+ "table": "users",
+ "entityType": "uniques"
+ }
+ ],
+ "renames": []
+}
\ No newline at end of file
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index 1b91bdc..551833a 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -357,3 +357,20 @@ export const verifications = pgTable(
},
(table) => [index("verifications_identifier_idx").on(table.identifier)],
);
+
+// Links admins hand out so someone can register while the instance is invite
+// only. The token is the secret part of the link.
+export const invites = pgTable(
+ "invites",
+ {
+ id: text("id").primaryKey().$defaultFn(nanoid),
+ token: text("token").notNull().unique(),
+ createdBy: text("created_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ maxUses: integer("max_uses").notNull().default(1),
+ uses: integer("uses").notNull().default(0),
+ expiresAt: timestamptz("expires_at").notNull(),
+ createdAt: timestamptz("created_at").defaultNow().notNull(),
+ },
+);
diff --git a/lib/auth.ts b/lib/auth.ts
index 255cc5d..ef2f24e 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -1,6 +1,6 @@
import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";
import { db } from "../server/lib/db";
-import { APIError, createAuthMiddleware, getSessionFromCtx } from 'better-auth/api';
+import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from 'better-auth/api';
import { betterAuth } from 'better-auth/minimal';
import { createAccessControl } from 'better-auth/plugins/access';
import { admin } from 'better-auth/plugins/admin';
@@ -8,6 +8,7 @@ import { defaultStatements } from 'better-auth/plugins/admin/access';
import { twoFactor } from 'better-auth/plugins/two-factor';
import * as schema from '../drizzle/schema';
import { getRegistrationMode } from '../server/utils/instance';
+import { claimInvite, INVITE_TOKEN_HEADER, releaseInvite } from '../server/utils/invites';
const MIN_PASSWORD_LENGTH = 8;
@@ -51,12 +52,26 @@ export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
// Who may register is an instance setting admins change at runtime.
- // Invite links do not exist yet, so only open instances take sign ups.
- if (ctx.path === '/sign-up/email' && await getRegistrationMode() !== 'open') {
- throw new APIError('FORBIDDEN', {
- message: 'Registration is closed on this instance',
- code: 'REGISTRATION_CLOSED',
- });
+ if (ctx.path === '/sign-up/email') {
+ const mode = await getRegistrationMode();
+
+ if (mode === 'closed') {
+ throw new APIError('FORBIDDEN', {
+ message: 'Registration is closed on this instance',
+ code: 'REGISTRATION_CLOSED',
+ });
+ }
+
+ if (mode === 'invite') {
+ const token = ctx.headers?.get(INVITE_TOKEN_HEADER);
+
+ if (!token || !await claimInvite(token)) {
+ throw new APIError('FORBIDDEN', {
+ message: 'This invite link is not valid anymore',
+ code: 'INVITE_INVALID',
+ });
+ }
+ }
}
// creating a user as an admin skips the sign up password rules
@@ -89,6 +104,17 @@ export const auth = betterAuth({
});
}
}),
+
+ // The invite was claimed before the account was made. When making it
+ // then fails, say because the email is taken, the use is given back.
+ after: createAuthMiddleware(async (ctx) => {
+ if (ctx.path !== '/sign-up/email' || !isAPIError(ctx.context.returned)) return;
+
+ const token = ctx.headers?.get(INVITE_TOKEN_HEADER);
+ if (token && await getRegistrationMode() === 'invite') {
+ await releaseInvite(token);
+ }
+ }),
},
plugins: [
diff --git a/server/api/admin/invites/[id].delete.ts b/server/api/admin/invites/[id].delete.ts
new file mode 100644
index 0000000..d41acd8
--- /dev/null
+++ b/server/api/admin/invites/[id].delete.ts
@@ -0,0 +1,11 @@
+import { eq } from 'drizzle-orm';
+import { invites } from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ await db.delete(invites).where(eq(invites.id, getRouterParam(event, 'id')!));
+
+ return { ok: true };
+});
diff --git a/server/api/admin/invites/index.get.ts b/server/api/admin/invites/index.get.ts
new file mode 100644
index 0000000..1760f5e
--- /dev/null
+++ b/server/api/admin/invites/index.get.ts
@@ -0,0 +1,21 @@
+import { asc } from 'drizzle-orm';
+import { invites } from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ await deleteDeadInvites();
+
+ return await db
+ .select({
+ id: invites.id,
+ token: invites.token,
+ maxUses: invites.maxUses,
+ uses: invites.uses,
+ expiresAt: invites.expiresAt,
+ createdAt: invites.createdAt,
+ })
+ .from(invites)
+ .orderBy(asc(invites.createdAt));
+});
diff --git a/server/api/admin/invites/index.post.ts b/server/api/admin/invites/index.post.ts
new file mode 100644
index 0000000..804f6d0
--- /dev/null
+++ b/server/api/admin/invites/index.post.ts
@@ -0,0 +1,28 @@
+import { nanoid } from 'nanoid';
+import { invites } from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+const TOKEN_LENGTH = 32;
+
+/** Creates an invite link that lets one person register within a week. */
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ const [invite] = await db.insert(invites)
+ .values({
+ token: nanoid(TOKEN_LENGTH),
+ createdBy: event.context.user!.id as string,
+ maxUses: 1,
+ expiresAt: new Date(Date.now() + INVITE_LIFETIME_MS),
+ })
+ .returning({
+ id: invites.id,
+ token: invites.token,
+ maxUses: invites.maxUses,
+ uses: invites.uses,
+ expiresAt: invites.expiresAt,
+ createdAt: invites.createdAt,
+ });
+
+ return invite!;
+});
diff --git a/server/api/invite/[token].get.ts b/server/api/invite/[token].get.ts
new file mode 100644
index 0000000..fe51c12
--- /dev/null
+++ b/server/api/invite/[token].get.ts
@@ -0,0 +1,22 @@
+/**
+ * Tells the register page whether an invite link still works and who it is
+ * from. Public on purpose, the person opening the link has no account yet.
+ */
+export default defineEventHandler(async (event) => {
+ const invite = await findUsableInvite(getRouterParam(event, 'token')!);
+
+ if (!invite) {
+ return { valid: false as const };
+ }
+
+ return {
+ valid: true as const,
+ expiresAt: invite.expiresAt,
+ singleUse: invite.maxUses === 1,
+ inviter: {
+ id: invite.inviterId,
+ name: invite.inviterName,
+ image: invite.inviterImage,
+ },
+ };
+});
diff --git a/server/utils/invites.ts b/server/utils/invites.ts
new file mode 100644
index 0000000..ce986a0
--- /dev/null
+++ b/server/utils/invites.ts
@@ -0,0 +1,52 @@
+import { and, eq, gt, lt, lte, or, sql } from 'drizzle-orm';
+import { invites, users } from '../../drizzle/schema';
+import { db } from '../lib/db';
+
+export const INVITE_TOKEN_HEADER = 'x-invite-token';
+export const INVITE_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
+
+// still has uses left and has not run out
+const usable = () => and(lt(invites.uses, invites.maxUses), gt(invites.expiresAt, new Date()));
+
+/**
+ * Takes one use of an invite. It is a single statement, so two people
+ * registering with the same single use link at once cannot both get in.
+ */
+export const claimInvite = async (token: string): Promise => {
+ const claimed = await db.update(invites)
+ .set({ uses: sql`${invites.uses} + 1` })
+ .where(and(eq(invites.token, token), usable()))
+ .returning({ id: invites.id });
+
+ return claimed.length > 0;
+};
+
+/** Gives a use back when the registration it was taken for did not go through. */
+export const releaseInvite = async (token: string) => {
+ await db.update(invites)
+ .set({ uses: sql`${invites.uses} - 1` })
+ .where(and(eq(invites.token, token), gt(invites.uses, 0)));
+};
+
+export const findUsableInvite = async (token: string) => {
+ const rows = await db
+ .select({
+ expiresAt: invites.expiresAt,
+ maxUses: invites.maxUses,
+ uses: invites.uses,
+ inviterId: users.id,
+ inviterName: users.name,
+ inviterImage: users.image,
+ })
+ .from(invites)
+ .innerJoin(users, eq(users.id, invites.createdBy))
+ .where(and(eq(invites.token, token), usable()))
+ .limit(1);
+
+ return rows[0] ?? null;
+};
+
+/** Used up and expired invites are of no use to anyone, so they are dropped. */
+export const deleteDeadInvites = async () => {
+ await db.delete(invites).where(or(lte(invites.expiresAt, new Date()), sql`${invites.uses} >= ${invites.maxUses}`));
+};