-
+
{{ group.label }}
+
+ Admin only
+
diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts
index 798c412..820f11f 100644
--- a/app/composables/useAuth.ts
+++ b/app/composables/useAuth.ts
@@ -1,5 +1,5 @@
import type { BetterAuthClientOptions, InferSessionFromClient, InferUserFromClient, User } from 'better-auth/client';
-import { twoFactorClient } from 'better-auth/client/plugins';
+import { adminClient, twoFactorClient } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/vue';
import { Err, Ok, type Result } from '~~/types/result';
import { assert } from '~~/utils/assert';
@@ -31,7 +31,7 @@ export type SignInResult =
export const useAuth = () => {
const authClient = createAuthClient({
- plugins: [twoFactorClient()],
+ plugins: [twoFactorClient(), adminClient()],
});
const session = useState | null>('auth:session', () => null);
const user = useState('auth:user', () => null);
diff --git a/app/composables/useDialog.ts b/app/composables/useDialog.ts
index c83ca94..bc1eb8d 100644
--- a/app/composables/useDialog.ts
+++ b/app/composables/useDialog.ts
@@ -7,6 +7,7 @@ export enum DialogType {
ChangePassword = 'change-password',
TwoFactor = 'two-factor',
DeleteAccount = 'delete-account',
+ CreateUser = 'create-user',
}
interface DialogOptions {
diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts
index 98f4129..0d2792b 100644
--- a/app/middleware/auth.global.ts
+++ b/app/middleware/auth.global.ts
@@ -31,7 +31,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
// once an account exists the answer never changes back
if (setupNeeded.value !== false) {
try {
- setupNeeded.value = (await $fetch<{ needed: boolean }>('/api/setup')).needed;
+ setupNeeded.value = (await $fetch<{ setupNeeded: boolean }>('/api/instance')).setupNeeded;
} catch (error) {
console.error('Failed to check instance setup:', error);
setupNeeded.value = null;
diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue
index ac21334..ea41cc0 100644
--- a/app/pages/auth/login.vue
+++ b/app/pages/auth/login.vue
@@ -14,7 +14,8 @@ if (session.value !== null) {
await navigateTo(to ?? '/');
}
-const config = useRuntimeConfig();
+// who may register is an instance setting, admins change it while it runs
+const { data: instance } = await useFetch('/api/instance', { key: 'instance' });
const form = reactive({
email: '',
@@ -110,6 +111,10 @@ const submit = async () => {
emailInputEl.value!.setCustomValidity(res.error.data.message!);
emailInputEl.value!.reportValidity();
break;
+ case 'BANNED_USER':
+ emailInputEl.value!.setCustomValidity('This account has been disabled by an admin of this instance.');
+ emailInputEl.value!.reportValidity();
+ break;
default:
console.log(res.error);
alert(`Something went wrong. ${res.error.data.message}`);
@@ -303,7 +308,7 @@ const submitTwoFactor = async () => {
-
+
New to Veridian?
diff --git a/app/pages/auth/register.vue b/app/pages/auth/register.vue
index 003b911..fdb4eff 100644
--- a/app/pages/auth/register.vue
+++ b/app/pages/auth/register.vue
@@ -12,9 +12,10 @@ if (session.value !== null) {
await navigateTo(to ?? '/');
}
-const config = useRuntimeConfig();
+// who may register is an instance setting, admins change it while it runs
+const { data: instance } = await useFetch('/api/instance', { key: 'instance' });
-if (config.public.disableSignup) {
+if (instance.value && instance.value.registration !== 'open') {
await navigateTo(to ? `/auth/login?to=${to}` : '/auth/login');
}
@@ -121,6 +122,10 @@ const submit = async () => {
passwordInputEl.value!.setCustomValidity(res.error.data.message!);
passwordInputEl.value!.reportValidity();
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.');
+ break;
default:
console.log(res.error);
alert('Something went wrong: ' + res.error.data.message);
diff --git a/app/pages/settings.vue b/app/pages/settings.vue
index 378714d..7c96034 100644
--- a/app/pages/settings.vue
+++ b/app/pages/settings.vue
@@ -10,6 +10,7 @@ const TITLES: Record = {
appearance: 'Appearance',
providers: 'AI Providers',
assistants: 'System Assistants',
+ instance: 'Instance',
};
const title = computed(() => {
@@ -20,6 +21,8 @@ const title = computed(() => {
return provider ? provider.name : 'Unknown Provider';
}
+ if (section === 'instance' && id === 'users') return 'Users';
+
return TITLES[section ?? ''] ?? 'Settings';
});
diff --git a/app/pages/settings/instance/index.vue b/app/pages/settings/instance/index.vue
new file mode 100644
index 0000000..3ae98bd
--- /dev/null
+++ b/app/pages/settings/instance/index.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
diff --git a/app/pages/settings/instance/users.vue b/app/pages/settings/instance/users.vue
new file mode 100644
index 0000000..fb40981
--- /dev/null
+++ b/app/pages/settings/instance/users.vue
@@ -0,0 +1,348 @@
+
+
+
+
+
diff --git a/drizzle/migrations/20261008182241_instance-settings/migration.sql b/drizzle/migrations/20261008182241_instance-settings/migration.sql
new file mode 100644
index 0000000..81d4875
--- /dev/null
+++ b/drizzle/migrations/20261008182241_instance-settings/migration.sql
@@ -0,0 +1,5 @@
+CREATE TABLE "instance_settings" (
+ "id" text PRIMARY KEY,
+ "registration" text DEFAULT 'open' NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
diff --git a/drizzle/migrations/20261008182241_instance-settings/snapshot.json b/drizzle/migrations/20261008182241_instance-settings/snapshot.json
new file mode 100644
index 0000000..18abdf2
--- /dev/null
+++ b/drizzle/migrations/20261008182241_instance-settings/snapshot.json
@@ -0,0 +1,3094 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "2a4edef1-7e2b-45fe-aa8e-f7c8b32de18d",
+ "prevIds": [
+ "7ef2914b-4030-48de-a10a-d32840bc463c"
+ ],
+ "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": "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": "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": [
+ "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": "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": "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 80d5003..1b91bdc 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -25,6 +25,19 @@ export enum ToolCallType {
Json = 'json',
}
+export const REGISTRATION_MODES = ['open', 'invite', 'closed'] as const;
+export type RegistrationMode = typeof REGISTRATION_MODES[number];
+
+export const INSTANCE_SETTINGS_ID = 'instance';
+
+// Settings of the whole instance, changed by admins. There is only ever the
+// one row with the id above.
+export const instanceSettings = pgTable('instance_settings', {
+ id: text('id').primaryKey(),
+ registration: text('registration').$type().notNull().default('open'),
+ updatedAt: timestamptz('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
+});
+
export const settings = pgTable('settings', {
id: text('id').primaryKey().$defaultFn(nanoid),
userId: text('user_id').notNull(),
diff --git a/lib/auth.ts b/lib/auth.ts
index 59f4a18..255cc5d 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -7,15 +7,20 @@ import { admin } from 'better-auth/plugins/admin';
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';
+
+const MIN_PASSWORD_LENGTH = 8;
const accessControl = createAccessControl(defaultStatements);
// Admins manage accounts, not content. Signing in as someone else and setting
// another user's password or email are left out on purpose: the first would
// show their chats, the others would lock them out of their encrypted API keys.
+// Deleting is left out too, because Better Auth would only remove the account
+// and leave its agents, topics and files behind. /api/admin/users does that.
const roles = {
admin: accessControl.newRole({
- user: ['create', 'list', 'set-role', 'ban', 'delete', 'get', 'update'],
+ user: ['create', 'list', 'set-role', 'ban', 'get', 'update'],
session: ['list', 'revoke', 'delete'],
}),
user: accessControl.newRole({
@@ -44,10 +49,30 @@ export const auth = betterAuth({
trustedOrigins: [process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000'],
hooks: {
- // The admin endpoints already stop admins from removing or disabling
- // themselves. Giving up their own admin role is the one way left to
- // end up with an instance nobody can manage, so that is refused too.
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',
+ });
+ }
+
+ // creating a user as an admin skips the sign up password rules
+ if (ctx.path === '/admin/create-user') {
+ const password = ctx.body?.password;
+ if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
+ throw new APIError('BAD_REQUEST', {
+ message: 'Password is too short',
+ code: 'PASSWORD_TOO_SHORT',
+ });
+ }
+ }
+
+ // The admin endpoints already stop admins from removing or disabling
+ // themselves. Giving up their own admin role is the one way left to
+ // end up with an instance nobody can manage, so that is refused too.
if (ctx.path !== '/admin/set-role' && ctx.path !== '/admin/update-user') return;
const role = ctx.path === '/admin/set-role' ? ctx.body?.role : ctx.body?.data?.role;
diff --git a/server/api/account/index.delete.ts b/server/api/account/index.delete.ts
index e86fe44..3f8155a 100644
--- a/server/api/account/index.delete.ts
+++ b/server/api/account/index.delete.ts
@@ -1,26 +1,12 @@
import { and, eq, ne } from 'drizzle-orm';
import * as z from 'zod';
-import {
- agents,
- files,
- models,
- providers,
- settings,
- toolCalls,
- topics,
- users,
-} from '~~/drizzle/schema';
+import { users } from '~~/drizzle/schema';
import { auth } from '~~/lib/auth';
import { db } from '~~/server/lib/db';
/**
- * Deletes the signed in user's account and everything that belongs to it.
- *
- * The app tables only store a plain user id, nothing cascades from the users
- * table, so every one of them is cleared here. It all happens in one
- * transaction: either the whole account is gone or nothing changed.
- *
- * Objects in file storage are left alone on purpose.
+ * Deletes the signed in user's account and everything that belongs to it,
+ * after they proved it is really them.
*/
export default defineEventHandler(async (event) => {
await protectRoute(event);
@@ -125,21 +111,7 @@ export default defineEventHandler(async (event) => {
});
}
- await db.transaction(async (tx) => {
- // messages, their parts, generations and attachments cascade from topics
- await tx.delete(topics).where(eq(topics.userId, userId));
- await tx.delete(agents).where(eq(agents.userId, userId));
- await tx.delete(models).where(eq(models.userId, userId));
- await tx.delete(providers).where(eq(providers.userId, userId));
- // nothing points a tool call back at its message, so these never cascade
- await tx.delete(toolCalls).where(eq(toolCalls.userId, userId));
- // embeddings go with their file
- await tx.delete(files).where(eq(files.userId, userId));
- await tx.delete(settings).where(eq(settings.userId, userId));
-
- // sessions, accounts and two-factor rows cascade from the user
- await tx.delete(users).where(eq(users.id, userId));
- });
+ await deleteAccount(userId);
// the session is gone, but the browser would keep trusting its cached
// copy for a few minutes unless the cookies go too
diff --git a/server/api/admin/instance.get.ts b/server/api/admin/instance.get.ts
new file mode 100644
index 0000000..eaabd6d
--- /dev/null
+++ b/server/api/admin/instance.get.ts
@@ -0,0 +1,9 @@
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ return {
+ registration: await getRegistrationMode(),
+ // set when DISABLE_SIGNUP in the environment overrules the setting
+ registrationLocked: isRegistrationLocked(),
+ };
+});
diff --git a/server/api/admin/instance.put.ts b/server/api/admin/instance.put.ts
new file mode 100644
index 0000000..d061887
--- /dev/null
+++ b/server/api/admin/instance.put.ts
@@ -0,0 +1,43 @@
+import * as z from 'zod';
+import { INSTANCE_SETTINGS_ID, REGISTRATION_MODES, instanceSettings } from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ const result = await readValidatedBody(event, (body) =>
+ z
+ .object({
+ registration: z.enum(REGISTRATION_MODES),
+ })
+ .safeParse(body),
+ );
+
+ if (!result.success) {
+ throw createError({
+ statusCode: 400,
+ message: result.error.issues[0]!.message,
+ });
+ }
+
+ if (isRegistrationLocked()) {
+ throw createError({
+ statusCode: 409,
+ statusMessage: 'Registration is locked by the environment',
+ data: {
+ code: 'REGISTRATION_LOCKED',
+ },
+ });
+ }
+
+ const { registration } = result.data;
+
+ await db.insert(instanceSettings)
+ .values({ id: INSTANCE_SETTINGS_ID, registration })
+ .onConflictDoUpdate({
+ target: instanceSettings.id,
+ set: { registration, updatedAt: new Date() },
+ });
+
+ return { registration, registrationLocked: false };
+});
diff --git a/server/api/admin/users/[id]/index.delete.ts b/server/api/admin/users/[id]/index.delete.ts
new file mode 100644
index 0000000..0fe1021
--- /dev/null
+++ b/server/api/admin/users/[id]/index.delete.ts
@@ -0,0 +1,40 @@
+import { db } from '~~/server/lib/db';
+
+/**
+ * Deletes someone else's account and everything that belongs to it.
+ *
+ * Admins delete their own account from their profile, where it asks for the
+ * password and keeps the last admin from leaving.
+ */
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ const id = getRouterParam(event, 'id')!;
+
+ if (id === event.context.user!.id) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'You cannot delete your own account here',
+ data: {
+ code: 'CANNOT_DELETE_YOURSELF',
+ },
+ });
+ }
+
+ const account = await db.query.users.findFirst({
+ where: {
+ id,
+ },
+ });
+
+ if (!account) {
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'User not found',
+ });
+ }
+
+ await deleteAccount(id);
+
+ return { ok: true };
+});
diff --git a/server/api/admin/users/[id]/two-factor.delete.ts b/server/api/admin/users/[id]/two-factor.delete.ts
new file mode 100644
index 0000000..2c3f75a
--- /dev/null
+++ b/server/api/admin/users/[id]/two-factor.delete.ts
@@ -0,0 +1,34 @@
+import { eq } from 'drizzle-orm';
+import { twoFactors, users } from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+/**
+ * Turns two-factor sign-in off for a user who lost their authenticator app
+ * and backup codes. They sign in with just their password again and can set
+ * it up from scratch.
+ */
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ const id = getRouterParam(event, 'id')!;
+
+ const account = await db.query.users.findFirst({
+ where: {
+ id,
+ },
+ });
+
+ if (!account) {
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'User not found',
+ });
+ }
+
+ await db.transaction(async (tx) => {
+ await tx.delete(twoFactors).where(eq(twoFactors.userId, id));
+ await tx.update(users).set({ twoFactorEnabled: false }).where(eq(users.id, id));
+ });
+
+ return { ok: true };
+});
diff --git a/server/api/admin/users/index.get.ts b/server/api/admin/users/index.get.ts
new file mode 100644
index 0000000..c2f4d3f
--- /dev/null
+++ b/server/api/admin/users/index.get.ts
@@ -0,0 +1,39 @@
+import { asc, eq, max } from 'drizzle-orm';
+import { sessions, users } from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+/**
+ * Every account of the instance, for the admin user list.
+ *
+ * Only what is needed to manage accounts is returned. Chats, agents, files and
+ * API keys of other users are never part of this.
+ */
+export default defineEventHandler(async (event) => {
+ await protectAdminRoute(event);
+
+ const rows = await db
+ .select({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ image: users.image,
+ role: users.role,
+ banned: users.banned,
+ twoFactorEnabled: users.twoFactorEnabled,
+ createdAt: users.createdAt,
+ // sessions are touched as they are used, the newest one is the
+ // closest thing there is to a last seen time
+ lastActiveAt: max(sessions.updatedAt),
+ })
+ .from(users)
+ .leftJoin(sessions, eq(sessions.userId, users.id))
+ .groupBy(users.id)
+ .orderBy(asc(users.createdAt));
+
+ return rows.map((row) => ({
+ ...row,
+ role: row.role === 'admin' ? 'admin' as const : 'user' as const,
+ banned: !!row.banned,
+ twoFactorEnabled: !!row.twoFactorEnabled,
+ }));
+});
diff --git a/server/api/instance.get.ts b/server/api/instance.get.ts
new file mode 100644
index 0000000..1ed6a49
--- /dev/null
+++ b/server/api/instance.get.ts
@@ -0,0 +1,6 @@
+// Public on purpose: the sign in pages ask this before anyone is signed in.
+export default defineEventHandler(async () => {
+ const [setupNeeded, registration] = await Promise.all([isSetupNeeded(), getRegistrationMode()]);
+
+ return { setupNeeded, registration };
+});
diff --git a/server/api/setup/index.get.ts b/server/api/setup/index.get.ts
deleted file mode 100644
index 7fc974b..0000000
--- a/server/api/setup/index.get.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Public on purpose: the sign in pages ask this before anyone has an account.
-export default defineEventHandler(async () => {
- return { needed: await isSetupNeeded() };
-});
diff --git a/server/utils/account.ts b/server/utils/account.ts
new file mode 100644
index 0000000..2aa099e
--- /dev/null
+++ b/server/utils/account.ts
@@ -0,0 +1,39 @@
+import { eq } from 'drizzle-orm';
+import {
+ agents,
+ files,
+ models,
+ providers,
+ settings,
+ toolCalls,
+ topics,
+ users,
+} from '~~/drizzle/schema';
+import { db } from '~~/server/lib/db';
+
+/**
+ * Deletes an account and everything that belongs to it.
+ *
+ * The app tables only store a plain user id, nothing cascades from the users
+ * table, so every one of them is cleared here. It all happens in one
+ * transaction: either the whole account is gone or nothing changed.
+ *
+ * Objects in file storage are left alone on purpose.
+ */
+export const deleteAccount = async (userId: string) => {
+ await db.transaction(async (tx) => {
+ // messages, their parts, generations and attachments cascade from topics
+ await tx.delete(topics).where(eq(topics.userId, userId));
+ await tx.delete(agents).where(eq(agents.userId, userId));
+ await tx.delete(models).where(eq(models.userId, userId));
+ await tx.delete(providers).where(eq(providers.userId, userId));
+ // nothing points a tool call back at its message, so these never cascade
+ await tx.delete(toolCalls).where(eq(toolCalls.userId, userId));
+ // embeddings go with their file
+ await tx.delete(files).where(eq(files.userId, userId));
+ await tx.delete(settings).where(eq(settings.userId, userId));
+
+ // sessions, accounts and two-factor rows cascade from the user
+ await tx.delete(users).where(eq(users.id, userId));
+ });
+};
diff --git a/server/utils/admin.ts b/server/utils/admin.ts
new file mode 100644
index 0000000..8a2ba72
--- /dev/null
+++ b/server/utils/admin.ts
@@ -0,0 +1,26 @@
+import type { H3Event } from 'h3';
+import { db } from '~~/server/lib/db';
+
+/**
+ * Like protectRoute(), but only lets admins through.
+ *
+ * The role is read from the database: the session cookie keeps a copy of the
+ * user for a few minutes, and someone who just stopped being an admin should
+ * not keep the access for that long.
+ */
+export const protectAdminRoute = async (event: H3Event) => {
+ await protectRoute(event);
+
+ const account = await db.query.users.findFirst({
+ where: {
+ id: event.context.user!.id as string,
+ },
+ });
+
+ if (account?.role !== 'admin' || account.banned) {
+ throw createError({
+ statusCode: 403,
+ statusMessage: 'Forbidden',
+ });
+ }
+};
diff --git a/server/utils/instance.ts b/server/utils/instance.ts
new file mode 100644
index 0000000..8b1bad3
--- /dev/null
+++ b/server/utils/instance.ts
@@ -0,0 +1,27 @@
+import { INSTANCE_SETTINGS_ID, REGISTRATION_MODES, type RegistrationMode } from '../../drizzle/schema';
+import { db } from '../lib/db';
+
+/**
+ * DISABLE_SIGNUP in the environment wins over whatever admins pick in the UI,
+ * so a deployment that was locked down this way stays locked down.
+ */
+export const isRegistrationLocked = (): boolean => {
+ const value = process.env.DISABLE_SIGNUP?.toLowerCase();
+
+ return value === 'true' || value === '1';
+};
+
+export const getRegistrationMode = async (): Promise => {
+ if (isRegistrationLocked()) return 'closed';
+
+ const row = await db.query.instanceSettings.findFirst({
+ where: {
+ id: INSTANCE_SETTINGS_ID,
+ },
+ });
+
+ const mode = row?.registration;
+
+ // instances that never touched the setting keep taking sign ups like before
+ return mode !== undefined && REGISTRATION_MODES.includes(mode) ? mode : 'open';
+};