feat: integrate SQLite support and enhance authentication logic#16
feat: integrate SQLite support and enhance authentication logic#16
Conversation
danships
commented
Jan 22, 2026
- Added SQLite database support by introducing a new database adapter that handles both SQLite and MySQL connection strings.
- Updated the authentication initialization to utilize the new database adapter.
- Modified the default database connection to use SQLite when no DB environment variable is set.
- Enhanced the login page with improved form handling and error notifications.
- Updated environment variables to include a flag for skipping automatic schema synchronization in production.
- Added SQLite database support by introducing a new database adapter that handles both SQLite and MySQL connection strings. - Updated the authentication initialization to utilize the new database adapter. - Modified the default database connection to use SQLite when no DB environment variable is set. - Enhanced the login page with improved form handling and error notifications. - Updated environment variables to include a flag for skipping automatic schema synchronization in production.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis PR adds SQLite database support alongside MySQL via better-sqlite3, introduces OIDC authentication alongside credential-based auth, implements a database migration system for better-auth tables, and updates login/signup flows with dynamic authentication mode selection based on environment configuration. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Client as Browser/Client
participant Server as Next.js Server
participant AuthAPI as Auth API
participant AuthProvider as OIDC/Auth Service
User->>Client: Visit /login
Client->>Server: GET /api/v1/config
Server->>Server: Check OIDC env vars
Server-->>Client: { authMode: 'oidc' | 'credentials' }
Client->>Client: Render form based on authMode
alt Credentials Mode
User->>Client: Enter email & password
Client->>AuthAPI: signIn.email(email, password)
AuthAPI-->>Client: Success / Error
Client->>Server: Redirect to / or show error
else OIDC Mode
User->>Client: Click Sign In with OIDC
Client->>AuthProvider: Initiate OAuth flow
AuthProvider-->>Client: Redirect with auth code
Client->>AuthAPI: Exchange code for session
AuthAPI-->>Client: Session established
Client->>Server: Redirect to /
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/app/`(auth)/login/page.tsx:
- Around line 48-61: Replace the raw fetch in the useEffect's fetchAuthConfig
with the project API client: import the exported API client (e.g., apiClient)
and call apiClient.get('/config') inside fetchAuthConfig, read the response.data
as GetAuthConfigResponse, then call setAuthMode(data.authMode); keep the
existing try/catch behavior (default to 'credentials' on error) and ensure you
update the import at the top and types accordingly so fetchAuthConfig,
setAuthMode, and GetAuthConfigResponse work with the axios-style response.data
shape.
- Around line 85-91: handleOidcLogin currently sets isOidcLoading true but never
resets it if authClient.signIn.social fails or redirect is blocked; make
handleOidcLogin async, await authClient.signIn.social inside a try/catch, and in
the catch call setIsOidcLoading(false) (and optionally log or surface the
error); you can also reset the loading state in a finally block if you prefer,
but ensure setIsOidcLoading(false) is executed on error so the button doesn't
stay stuck.
In `@src/lib/database/migrations/better-auth.ts`:
- Around line 1-11: The documentation comment in better-auth.ts contains a
hardcoded secret value; replace the literal secret in the example env export
(the `BETTER_AUTH_SECRET=4d675dd...64ac` token) with a clear placeholder like
`BETTER_AUTH_SECRET=<your_secret_here>` or `BETTER_AUTH_SECRET=your_secret_here`
so the example remains functional but does not include real secrets; update any
matching example text in the same comment block to use the placeholder and keep
the rest of the instructions intact.
In `@src/lib/database/migrations/index.ts`:
- Around line 6-29: The migration SQL uses plain CREATE TABLE/CREATE INDEX which
will error if re-run; update the SQL constants BETTER_AUTH_SQLITE_SQL and
BETTER_AUTH_MYSQL_SQL (referenced by the migrations array) to add IF NOT EXISTS
on every CREATE TABLE and CREATE INDEX statement so repeated runMigrations()
calls do not fail; ensure both the SQLite and MySQL SQL strings in
better-auth.ts are changed accordingly and keep statement splitting/execution
logic in the mysql run block unchanged.
🧹 Nitpick comments (3)
src/components/layout.tsx (1)
51-61: Move inline styles to CSS module.Per coding guidelines, component-specific styles should use CSS modules instead of inline styles. Consider extracting these styles to a
.module.cssfile.♻️ Suggested refactor
Create a CSS module file
layout.module.css:.logoutLink { text-decoration: none; color: inherit; font-weight: 500; font-size: 0.95rem; }Then update the component:
+import styles from './layout.module.css'; import Link from 'next/link';- <Link - href="/logout" - style={{ - textDecoration: 'none', - color: 'inherit', - fontWeight: 500, - fontSize: '0.95rem', - }} - > - Logout - </Link> + <Link href="/logout" className={styles.logoutLink}> + Logout + </Link>src/lib/auth/config.ts (1)
17-23: Consider validating unsupported connection string schemes.The function assumes any non-SQLite connection string is MySQL-compatible. If an unsupported scheme (e.g.,
postgres://) is accidentally passed, it will fail with a confusing MySQL connection error rather than a clear validation message.Proposed validation
function createDatabaseAdapter(connectionString: string) { if (connectionString.startsWith('sqlite://')) { const databasePath = connectionString.replace('sqlite://', ''); return new Database(databasePath); } + if (!connectionString.startsWith('mysql://')) { + throw new Error(`Unsupported database connection string. Expected sqlite:// or mysql:// but got: ${connectionString.split('://')[0]}://`); + } return createPool(connectionString); }src/lib/database/migrations/index.ts (1)
25-27: Consider wrapping MySQL migration in a transaction for atomicity.If a statement fails mid-migration, the database will be left in a partially migrated state. Wrapping in a transaction would allow rollback on failure.
Proposed transaction wrapper
run: async (superSave: SuperSave) => { const pool = superSave.getConnection<Pool>(); + const connection = await pool.getConnection(); + await connection.beginTransaction(); + try { // MySQL requires executing statements one at a time const statements = BETTER_AUTH_MYSQL_SQL.split(';') .map((s) => s.trim()) .filter((s) => s.length > 0); for (const statement of statements) { - await pool.query(statement); + await connection.query(statement); } + await connection.commit(); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } },
| useEffect(() => { | ||
| const fetchAuthConfig = async () => { | ||
| try { | ||
| const response = await fetch('/api/v1/config'); | ||
| const data: GetAuthConfigResponse = await response.json(); | ||
| setAuthMode(data.authMode); | ||
| } catch { | ||
| // Default to credentials if we can't fetch config | ||
| setAuthMode('credentials'); | ||
| } | ||
| }; | ||
|
|
||
| fetchAuthConfig(); | ||
| }, []); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use the API client instead of raw fetch.
Per coding guidelines, front-end code should call API routes through src/lib/api/client.ts which uses axios with baseURL /api/v1.
Proposed fix using API client
+import { apiClient } from '@/lib/api/client';
+
// In the useEffect:
- const response = await fetch('/api/v1/config');
- const data: GetAuthConfigResponse = await response.json();
+ const { data } = await apiClient.get<GetAuthConfigResponse>('/config');
setAuthMode(data.authMode);Based on coding guidelines, front-end code must call API routes through src/lib/api/client.ts.
🤖 Prompt for AI Agents
In `@src/app/`(auth)/login/page.tsx around lines 48 - 61, Replace the raw fetch in
the useEffect's fetchAuthConfig with the project API client: import the exported
API client (e.g., apiClient) and call apiClient.get('/config') inside
fetchAuthConfig, read the response.data as GetAuthConfigResponse, then call
setAuthMode(data.authMode); keep the existing try/catch behavior (default to
'credentials' on error) and ensure you update the import at the top and types
accordingly so fetchAuthConfig, setAuthMode, and GetAuthConfigResponse work with
the axios-style response.data shape.
src/app/(auth)/login/page.tsx
Outdated
| const handleOidcLogin = () => { | ||
| setIsOidcLoading(true); | ||
| authClient.signIn.social({ | ||
| provider: 'oidc', | ||
| callbackURL: `${globalThis.location.origin}/`, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
OIDC loading state is never reset if redirect fails.
If authClient.signIn.social fails or the redirect doesn't occur (e.g., popup blocked), isOidcLoading remains true and the button stays in a perpetual loading state.
Proposed fix with error handling
- const handleOidcLogin = () => {
+ const handleOidcLogin = async () => {
setIsOidcLoading(true);
- authClient.signIn.social({
- provider: 'oidc',
- callbackURL: `${globalThis.location.origin}/`,
- });
+ try {
+ await authClient.signIn.social({
+ provider: 'oidc',
+ callbackURL: `${globalThis.location.origin}/`,
+ });
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Failed to initiate sign in';
+ showError(errorMessage);
+ setIsOidcLoading(false);
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleOidcLogin = () => { | |
| setIsOidcLoading(true); | |
| authClient.signIn.social({ | |
| provider: 'oidc', | |
| callbackURL: `${globalThis.location.origin}/`, | |
| }); | |
| }; | |
| const { showNotification } = useNotification(); | |
| const handleOidcLogin = async () => { | |
| setIsOidcLoading(true); | |
| try { | |
| await authClient.signIn.social({ | |
| provider: 'oidc', | |
| callbackURL: `${globalThis.location.origin}/`, | |
| }); | |
| } catch (error) { | |
| const errorMessage = error instanceof Error ? error.message : 'Failed to initiate sign in'; | |
| showNotification({ message: errorMessage, title: 'Sign In Error' }); | |
| setIsOidcLoading(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In `@src/app/`(auth)/login/page.tsx around lines 85 - 91, handleOidcLogin
currently sets isOidcLoading true but never resets it if
authClient.signIn.social fails or redirect is blocked; make handleOidcLogin
async, await authClient.signIn.social inside a try/catch, and in the catch call
setIsOidcLoading(false) (and optionally log or surface the error); you can also
reset the loading state in a finally block if you prefer, but ensure
setIsOidcLoading(false) is executed on error so the button doesn't stay stuck.
| /** | ||
| * These migrations are generated using the better-auth CLI. | ||
| * ``` | ||
| * export DB=sqlite://:memory: | ||
| * export NODE_ENV=development | ||
| * export BETTER_AUTH_SECRET=4d675dd169730483103eb302793617397357b4b86a63e19eb77c9cda3d0d64ac | ||
| * npx @better-auth/cli generate --config ./src/lib/auth/better-auth.ts | ||
| * ``` | ||
| * | ||
| * to generate for mysql, replace DB= env var with a mysql alternative. | ||
| */ |
There was a problem hiding this comment.
Replace hardcoded secret with a placeholder in documentation.
The comment contains a hardcoded secret value (4d675dd169730483103eb302793617397357b4b86a63e19eb77c9cda3d0d64ac) which triggered the Gitleaks static analysis warning. While this is just documentation, it's best practice to use a placeholder to avoid:
- Accidental use in production
- Security scanner false positives
📝 Suggested fix
- * export BETTER_AUTH_SECRET=4d675dd169730483103eb302793617397357b4b86a63e19eb77c9cda3d0d64ac
+ * export BETTER_AUTH_SECRET=<your-secret-here>🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 6-6: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In `@src/lib/database/migrations/better-auth.ts` around lines 1 - 11, The
documentation comment in better-auth.ts contains a hardcoded secret value;
replace the literal secret in the example env export (the
`BETTER_AUTH_SECRET=4d675dd...64ac` token) with a clear placeholder like
`BETTER_AUTH_SECRET=<your_secret_here>` or `BETTER_AUTH_SECRET=your_secret_here`
so the example remains functional but does not include real secrets; update any
matching example text in the same comment block to use the placeholder and keep
the rest of the instructions intact.
| export const migrations: Migration[] = [ | ||
| { | ||
| name: 'better-auth-tables', | ||
| engine: 'sqlite', | ||
| run: async (superSave: SuperSave) => { | ||
| const database = superSave.getConnection<Database>(); | ||
| database.exec(BETTER_AUTH_SQLITE_SQL); | ||
| }, | ||
| }, | ||
| { | ||
| name: 'better-auth-tables', | ||
| engine: 'mysql', | ||
| run: async (superSave: SuperSave) => { | ||
| const pool = superSave.getConnection<Pool>(); | ||
| // MySQL requires executing statements one at a time | ||
| const statements = BETTER_AUTH_MYSQL_SQL.split(';') | ||
| .map((s) => s.trim()) | ||
| .filter((s) => s.length > 0); | ||
|
|
||
| for (const statement of statements) { | ||
| await pool.query(statement); | ||
| } | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's understand the SuperSave setup and see what files are involved
find . -type f -name "*.ts" -o -name "*.tsx" | grep -i "migration\|supersave\|database" | head -20Repository: danships/thoth
Length of output: 545
🏁 Script executed:
# Let's look at the migration index file to understand the structure better
cat -n src/lib/database/migrations/index.tsRepository: danships/thoth
Length of output: 1195
🏁 Script executed:
# Check where the SQL statements are defined
fd -type f -name "better-auth.ts"Repository: danships/thoth
Length of output: 228
🏁 Script executed:
# Look for SuperSave package information and how migrations are tracked
cat package.json | jq '.dependencies' | grep -i "supersave\|migrate"Repository: danships/thoth
Length of output: 87
🏁 Script executed:
# Check for any migration execution/runner code
fd -type f -name "*migration*" -o -name "*runner*" -o -name "*execute*" | grep -E "\.(ts|tsx)$"Repository: danships/thoth
Length of output: 228
🏁 Script executed:
cat -n src/lib/database/migrations/better-auth.tsRepository: danships/thoth
Length of output: 3776
🏁 Script executed:
# Let's search for any migration state tracking or database initialization
rg "migration" --type ts --type tsx -i | grep -v node_modules | head -15Repository: danships/thoth
Length of output: 85
🏁 Script executed:
# Check for SuperSave configuration or initialization
rg "SuperSave\|supersave" --type ts --type tsx | grep -v node_modulesRepository: danships/thoth
Length of output: 85
🌐 Web query:
SuperSave migration tracking beta version 1.0.0-beta4 idempotency
💡 Result:
I can help — could you share the repository/ docs/ release notes or a link for "SuperSave migration tracking 1.0.0-beta4"? Also say what you mean by "idempotency": do you want to confirm whether migrations are idempotent, reproduce a bug, or get guidance to make them idempotent?
🏁 Script executed:
# Find SuperSave initialization or configuration
rg "new SuperSave\|createSuperSave\|SuperSave\(" --type ts -A 5 | head -30Repository: danships/thoth
Length of output: 40
🏁 Script executed:
# Check database initialization/connection setup
find src -name "*.ts" -type f | xargs grep -l "SuperSave\|migrations" | head -10Repository: danships/thoth
Length of output: 221
🏁 Script executed:
# Look at the database index file for initialization
cat -n src/lib/database/index.tsRepository: danships/thoth
Length of output: 1627
🏁 Script executed:
# Check if there's any migration state table or tracking mechanism
rg "migration.*table\|_migrations\|applied.*migration" --type ts -iRepository: danships/thoth
Length of output: 40
🌐 Web query:
SuperSave package npm migration idempotency tracking
💡 Result:
Short answer: Supersave (the @supersave/* packages on npm) does not appear to provide a built‑in, opinionated migration system with automatic idempotency tracking; you should either (A) make migrations idempotent and store applied‑migration metadata yourself in a Supersave collection, or (B) use a dedicated migration library that persists migration history (a migrations table/collection) and run idempotent migration steps. [1]. (npmjs.com)
Practical options (concise):
- Make each migration idempotent (CREATE IF NOT EXISTS, guards, upserts) and write a small "migrations" collection/document that records migration id, hash, timestamp and status after successful run. This is simple with Supersave hooks/APIs. [1]. (npmjs.com)
- Use an existing migration tool that supports stored history and idempotency (examples: OneTable Migrate for DynamoDB or other DB‑specific migrators) and run it alongside Supersave; those tools persist applied migrations so re‑runs are safe. [2]. (npmjs.com)
- Follow idempotent SQL/DDL patterns (IF NOT EXISTS / existence checks / ALTER guarded by existence queries) or frameworks that recommend idempotent notebook/SQL migration patterns. [3]. (sprymd.org)
If you want, tell me:
- which database backend Supersave is using for you (SQLite/Postgres/DynamoDB/etc.), and I’ll suggest a concrete migration + tracking implementation and example code.
Citations:
- 1: https://www.npmjs.com/package/%40supersave/auth?utm_source=openai
- 2: https://www.npmjs.com/package/%40supersave/auth?utm_source=openai
- 3: https://www.npmjs.com/package/onetable-migrate?utm_source=openai
- 4: https://sprymd.org/sql-migrations?utm_source=openai
🏁 Script executed:
# Check how runMigrations is implemented or if there's error handling
rg "runMigrations\|applyMigration\|executeMigration" --type ts -B 2 -A 5 | head -40Repository: danships/thoth
Length of output: 40
🏁 Script executed:
# Check if there's a migrations tracking table or collection
rg "migrations\|_applied\|migration.*log" --type ts -i | grep -v node_modules | head -20Repository: danships/thoth
Length of output: 40
🏁 Script executed:
# Look for any error handling around migration execution
cat -n src/lib/database/index.ts | grep -A 10 -B 5 "runMigrations"Repository: danships/thoth
Length of output: 556
🏁 Script executed:
# Check if migrations could be run multiple times
rg "getDatabase\|runMigrations" --type ts | head -20Repository: danships/thoth
Length of output: 40
Add IF NOT EXISTS to migration SQL to prevent failures on re-execution.
SuperSave does not track applied migrations or prevent re-execution. Both SQLite and MySQL SQL statements use plain CREATE TABLE and CREATE INDEX without IF NOT EXISTS guards. If runMigrations() is called a second time (e.g., during testing, manual triggering, or misconfiguration), these statements will fail with "table already exists" errors.
Modify the SQL statements in better-auth.ts to include IF NOT EXISTS on all CREATE TABLE and CREATE INDEX statements.
🤖 Prompt for AI Agents
In `@src/lib/database/migrations/index.ts` around lines 6 - 29, The migration SQL
uses plain CREATE TABLE/CREATE INDEX which will error if re-run; update the SQL
constants BETTER_AUTH_SQLITE_SQL and BETTER_AUTH_MYSQL_SQL (referenced by the
migrations array) to add IF NOT EXISTS on every CREATE TABLE and CREATE INDEX
statement so repeated runMigrations() calls do not fail; ensure both the SQLite
and MySQL SQL strings in better-auth.ts are changed accordingly and keep
statement splitting/execution logic in the mysql run block unchanged.