fix(todo): use TEXT for user_id columns (Better Auth compatibility)

Better Auth generates non-UUID user IDs (e.g., otUe1YrfENPdHnrF3g1vSBfpkQfambCZ).
Changed all user_id columns from uuid to text type to prevent
"invalid input syntax for type uuid" errors.

Also documented this requirement in:
- .claude/guidelines/authentication.md (new User ID Format section)
- .claude/guidelines/database.md (User ID Column Type section)
- apps/todo/CLAUDE.md (Database Schema section)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Wuesteon 2025-12-09 16:24:22 +01:00
parent 582c6f58a4
commit 4e63f3f74b
9 changed files with 87 additions and 16 deletions

View file

@ -32,11 +32,37 @@ All authentication is handled by **Mana Core Auth**, a centralized authenticatio
<──────────────────────│ │
```
## User ID Format
**CRITICAL**: Mana Core Auth uses Better Auth, which generates **non-UUID user IDs**.
```
Example user ID: otUe1YrfENPdHnrF3g1vSBfpkQfambCZ
```
**Format details:**
- 32 characters
- Base62 alphabet (a-z, A-Z, 0-9)
- ~190 bits of entropy (more than UUID's 122 bits)
- NOT a valid UUID format
**Database schema implications:**
```typescript
// CORRECT - use text for user_id
userId: text('user_id').notNull(),
// WRONG - will cause "invalid input syntax for type uuid" errors
userId: uuid('user_id').notNull(),
```
Always use `text` type for `user_id` columns in all database schemas.
## Token Structure (EdDSA JWT)
```json
{
"sub": "user-uuid-123",
"sub": "otUe1YrfENPdHnrF3g1vSBfpkQfambCZ",
"email": "user@example.com",
"role": "user",
"sid": "session-id-456",
@ -47,6 +73,8 @@ All authentication is handled by **Mana Core Auth**, a centralized authenticatio
}
```
**Note**: The `sub` claim contains the Better Auth user ID (not a UUID).
**Important**: Keep claims minimal. Do NOT include:
- Credit balance (changes frequently)
- Organization data (use API instead)

View file

@ -101,6 +101,26 @@ src/db/
└── migrations/ # Generated migrations
```
### User ID Column Type
**CRITICAL**: Always use `text` for `user_id` columns, NOT `uuid`.
Mana Core Auth (Better Auth) generates non-UUID user IDs:
```
Example: otUe1YrfENPdHnrF3g1vSBfpkQfambCZ
```
```typescript
// CORRECT
userId: text('user_id').notNull(),
// WRONG - causes "invalid input syntax for type uuid" errors
userId: uuid('user_id').notNull(),
```
See [Authentication Guidelines](./authentication.md#user-id-format) for details.
### Table Definition Pattern
```typescript
@ -123,8 +143,10 @@ export const files = pgTable(
// Primary key - always UUID with auto-generation
id: uuid('id').primaryKey().defaultRandom(),
// Foreign keys
userId: varchar('user_id', { length: 255 }).notNull(),
// User ID - always TEXT (Better Auth uses non-UUID format)
userId: text('user_id').notNull(),
// Foreign keys to other tables (UUIDs are fine)
parentFolderId: uuid('parent_folder_id').references(() => folders.id, { onDelete: 'set null' }),
// Required fields

View file

@ -156,9 +156,11 @@ pnpm preview # Preview build
## Database Schema
> **Note**: `user_id` columns use TEXT type (not UUID) because Mana Core Auth generates non-UUID user IDs.
### projects
- `id` (UUID) - Primary key
- `user_id` (UUID) - Owner
- `user_id` (TEXT) - Owner (Better Auth format)
- `name` (VARCHAR) - Project name
- `color` (VARCHAR) - Hex color
- `icon` (VARCHAR) - Icon name
@ -169,7 +171,7 @@ pnpm preview # Preview build
### tasks
- `id` (UUID) - Primary key
- `project_id` (UUID) - FK to projects (nullable = Inbox)
- `user_id` (UUID) - Owner
- `user_id` (TEXT) - Owner (Better Auth format)
- `title` (VARCHAR) - Task title
- `description` (TEXT) - Description
- `due_date` (TIMESTAMP) - Due date
@ -182,7 +184,7 @@ pnpm preview # Preview build
### labels
- `id` (UUID) - Primary key
- `user_id` (UUID) - Owner
- `user_id` (TEXT) - Owner (Better Auth format)
- `name` (VARCHAR) - Label name
- `color` (VARCHAR) - Hex color
@ -193,6 +195,7 @@ pnpm preview # Preview build
### reminders
- `id` (UUID) - Primary key
- `task_id` (UUID) - FK to tasks
- `user_id` (TEXT) - Owner (Better Auth format)
- `minutes_before` (INTEGER) - Offset
- `type` (VARCHAR) - push/email/both
- `status` (VARCHAR) - pending/sent/failed

View file

@ -1,11 +1,20 @@
import { pgTable, uuid, timestamp, varchar, boolean, integer, index } from 'drizzle-orm/pg-core';
import {
pgTable,
uuid,
text,
timestamp,
varchar,
boolean,
integer,
index,
} from 'drizzle-orm/pg-core';
import { projects } from './projects.schema';
export const kanbanBoards = pgTable(
'kanban_boards',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
userId: text('user_id').notNull(),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }),
// Board properties

View file

@ -1,4 +1,13 @@
import { pgTable, uuid, timestamp, varchar, boolean, integer, index } from 'drizzle-orm/pg-core';
import {
pgTable,
uuid,
text,
timestamp,
varchar,
boolean,
integer,
index,
} from 'drizzle-orm/pg-core';
import { kanbanBoards } from './kanban-boards.schema';
// Define locally to avoid circular dependency with tasks.schema
@ -8,7 +17,7 @@ export const kanbanColumns = pgTable(
'kanban_columns',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
userId: text('user_id').notNull(),
boardId: uuid('board_id')
.references(() => kanbanBoards.id, { onDelete: 'cascade' })
.notNull(),

View file

@ -1,10 +1,10 @@
import { pgTable, uuid, timestamp, varchar, index } from 'drizzle-orm/pg-core';
import { pgTable, uuid, text, timestamp, varchar, index } from 'drizzle-orm/pg-core';
export const labels = pgTable(
'labels',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
userId: text('user_id').notNull(),
name: varchar('name', { length: 100 }).notNull(),
color: varchar('color', { length: 7 }).default('#6B7280'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),

View file

@ -21,7 +21,7 @@ export const projects = pgTable(
'projects',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
userId: text('user_id').notNull(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
color: varchar('color', { length: 7 }).default('#3B82F6'),

View file

@ -1,4 +1,4 @@
import { pgTable, uuid, timestamp, varchar, integer, index } from 'drizzle-orm/pg-core';
import { pgTable, uuid, text, timestamp, varchar, integer, index } from 'drizzle-orm/pg-core';
import { tasks } from './tasks.schema';
export type ReminderType = 'push' | 'email' | 'both';
@ -11,7 +11,7 @@ export const reminders = pgTable(
taskId: uuid('task_id')
.notNull()
.references(() => tasks.id, { onDelete: 'cascade' }),
userId: uuid('user_id').notNull(),
userId: text('user_id').notNull(),
// Timing
minutesBefore: integer('minutes_before').notNull(),

View file

@ -45,7 +45,7 @@ export const tasks = pgTable(
{
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
userId: uuid('user_id').notNull(),
userId: text('user_id').notNull(),
parentTaskId: uuid('parent_task_id'),
// Content