Implement a WordPress-style setup wizard for the R2D2 application that allows users to configure the application on first run without requiring predefined environment variables. The configuration will be stored in a database instead of .env files.
- Drizzle ORM for database interactions
- NextAuth.js or Auth.js for authentication
- PostgreSQL (via Neon) for the database
- Server-side environment variables limited to:
AUTH_SECRET(for NextAuth.js)DATABASE_URL(for PostgreSQL connection)
-
Install required dependencies:
npm install drizzle-orm @neondatabase/serverless npm install -D drizzle-kit
-
Create database schema:
// db/schema.ts import { pgTable, serial, text, json, timestamp, boolean } from 'drizzle-orm/pg-core'; // User table export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), password: text('password').notNull(), // Hashed password role: text('role').notNull().default('admin'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); // App configuration table export const appConfig = pgTable('app_config', { id: serial('id').primaryKey(), key: text('key').notNull().unique(), value: text('value').notNull(), isSecret: boolean('is_secret').default(false).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), });
-
Create database connection client:
// db/index.ts import { neon } from '@neondatabase/serverless'; import { drizzle } from 'drizzle-orm/neon-http'; import * as schema from './schema'; const sql = neon(process.env.DATABASE_URL!); export const db = drizzle(sql, { schema });
-
Set up Drizzle migrations:
// drizzle.config.ts import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './db/schema.ts', out: './drizzle', driver: 'pg', dbCredentials: { connectionString: process.env.DATABASE_URL + '?sslmode=require', }, });
-
Install Auth.js:
npm install next-auth@beta @auth/drizzle-adapter
-
Create Auth.js configuration:
// auth.ts import NextAuth from 'next-auth'; import Credentials from 'next-auth/providers/credentials'; import { DrizzleAdapter } from '@auth/drizzle-adapter'; import { db } from '@/db'; import { compare } from 'bcrypt'; import { users } from '@/db/schema'; import { eq } from 'drizzle-orm'; export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: DrizzleAdapter(db), session: { strategy: 'jwt' }, pages: { signIn: '/auth/signin', }, providers: [ Credentials({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' } }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null; const user = await db.query.users.findFirst({ where: eq(users.email, credentials.email), }); if (!user) return null; const passwordMatch = await compare(credentials.password, user.password); if (!passwordMatch) return null; return { id: user.id.toString(), name: user.name, email: user.email, role: user.role, }; } }) ], callbacks: { async session({ session, token }) { if (token && session.user) { session.user.id = token.sub; session.user.role = token.role as string; } return session; }, async jwt({ token, user }) { if (user) { token.role = user.role; } return token; } }, });
-
Set up middleware for protected routes:
// middleware.ts export { auth as middleware } from '@/auth'; export const config = { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], };
-
Create setup check middleware:
// app/middleware.ts import { NextResponse } from 'next/server'; import { db } from '@/db'; import { appConfig } from '@/db/schema'; import { eq } from 'drizzle-orm'; export async function middleware(request: Request) { // Check if app is already setup const setupCompleted = await db.query.appConfig.findFirst({ where: eq(appConfig.key, 'setup_completed'), }); const isSetupRoute = request.url.includes('/setup'); // If setup is not completed and not on setup page, redirect to setup if (!setupCompleted && !isSetupRoute) { return NextResponse.redirect(new URL('/setup', request.url)); } // If setup is completed and on setup page, redirect to home if (setupCompleted && isSetupRoute) { return NextResponse.redirect(new URL('/', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], };
-
Create setup wizard pages:
- Step 1: Admin account creation
- Step 2: Cloudflare configuration
- Step 3: Setup completion
-
Implement setup actions:
// app/actions/setup.ts 'use server' import { db } from '@/db'; import { users, appConfig } from '@/db/schema'; import { hash } from 'bcrypt'; import { redirect } from 'next/navigation'; export async function createAdminUser(formData: FormData) { const name = formData.get('name') as string; const email = formData.get('email') as string; const password = formData.get('password') as string; if (!name || !email || !password) { return { error: 'All fields are required' }; } const hashedPassword = await hash(password, 10); try { await db.insert(users).values({ name, email, password: hashedPassword, role: 'admin', }); return { success: true }; } catch (error) { return { error: 'Failed to create admin user' }; } } export async function saveCloudflareConfig(formData: FormData) { const accountId = formData.get('accountId') as string; const accessKeyId = formData.get('accessKeyId') as string; const secretAccessKey = formData.get('secretAccessKey') as string; const apiToken = formData.get('apiToken') as string; const r2Endpoint = formData.get('r2Endpoint') as string; if (!accountId || !accessKeyId || !secretAccessKey || !apiToken || !r2Endpoint) { return { error: 'All fields are required' }; } try { // Store configuration in database const configs = [ { key: 'CLOUDFLARE_ACCOUNT_ID', value: accountId, isSecret: false }, { key: 'CLOUDFLARE_ACCESS_KEY_ID', value: accessKeyId, isSecret: true }, { key: 'CLOUDFLARE_SECRET_ACCESS_KEY', value: secretAccessKey, isSecret: true }, { key: 'CLOUDFLARE_API_TOKEN', value: apiToken, isSecret: true }, { key: 'CLOUDFLARE_R2_ENDPOINT', value: r2Endpoint, isSecret: false }, ]; for (const config of configs) { await db.insert(appConfig).values(config); } return { success: true }; } catch (error) { return { error: 'Failed to save Cloudflare configuration' }; } } export async function completeSetup() { try { await db.insert(appConfig).values({ key: 'setup_completed', value: 'true', isSecret: false, }); redirect('/'); } catch (error) { return { error: 'Failed to complete setup' }; } }
-
Create a service to retrieve configuration from the database:
// lib/config.ts import { cache } from 'react'; import { db } from '@/db'; import { appConfig } from '@/db/schema'; import { eq } from 'drizzle-orm'; export const getConfig = cache(async (key: string) => { const config = await db.query.appConfig.findFirst({ where: eq(appConfig.key, key), }); return config?.value || null; }); export const getCloudflareConfig = cache(async () => { const configs = await db.select().from(appConfig).where( eq(appConfig.key, 'CLOUDFLARE_ACCOUNT_ID') .or(eq(appConfig.key, 'CLOUDFLARE_ACCESS_KEY_ID')) .or(eq(appConfig.key, 'CLOUDFLARE_SECRET_ACCESS_KEY')) .or(eq(appConfig.key, 'CLOUDFLARE_API_TOKEN')) .or(eq(appConfig.key, 'CLOUDFLARE_R2_ENDPOINT')) ); const configMap: Record<string, string> = {}; for (const config of configs) { configMap[config.key] = config.value; } return configMap; });
-
Update existing API clients to use the configuration service:
// utils/r2-client.ts import { S3Client } from '@aws-sdk/client-s3'; import { getCloudflareConfig } from '@/lib/config'; export async function getR2Client() { const config = await getCloudflareConfig(); return new S3Client({ region: 'auto', endpoint: config.CLOUDFLARE_R2_ENDPOINT, credentials: { accessKeyId: config.CLOUDFLARE_ACCESS_KEY_ID, secretAccessKey: config.CLOUDFLARE_SECRET_ACCESS_KEY, }, }); }
-
Create step-by-step onboarding flow with TailwindCSS:
- Welcome screen
- Admin user creation form
- Cloudflare configuration form
- Success/completion screen
-
Implement validation and error handling:
- Form validation for all inputs
- Connection testing for Cloudflare credentials
- Clear error messages and recovery flows
-
Database Migration Strategy:
- Initialize database schema on first deployment
- Use Drizzle migrations for schema updates
- Handle migration failures gracefully
-
Security Considerations:
- Store sensitive values with encryption
- Implement proper authorization checks
- Protect setup routes from unauthorized access
- Use secure hashing for passwords
-
Multi-environment Support:
- Allow different configurations per environment
- Support development, staging, and production settings
-
Unit tests for:
- Configuration service
- Auth provider
- Setup wizard actions
-
Integration tests for:
- Setup flow completion
- Authentication flow
- Database schema migrations
-
End-to-end tests for:
- Complete setup wizard journey
- Authentication and authorization
-
Current Setup (.env file) to New Setup (database):
- Create database schema
- Implement setup wizard
- Add migration script to move existing .env variables to database
- Update API clients to use new configuration service
-
Fallback Mechanism:
- Support both .env and database configuration
- Prioritize database configuration over .env values
- Log warnings for deprecated .env usage
- Phase 1: Database Setup (2-3 days)
- Phase 2: Authentication Implementation (2-3 days)
- Phase 3: Setup Wizard Implementation (3-4 days)
- Phase 4: Environment Configuration Service (1-2 days)
- Phase 5: Setup Wizard UI (2-3 days)
- Testing and Refinement (2-3 days)
Total estimated time: 12-18 days