A self-hosted multi-tenant application that allows users to sign up and get their own unique subdomain profile page (e.g., username.localhost or username.example.com). Built with Next.js, TypeScript, SQLite, and Drizzle ORM.
- Unique Subdomains: Each user gets their own subdomain for their profile
- User Authentication: JWT-based authentication with secure session management
- Profile Management: Users can customize their display name, bio, and avatar
- Dynamic Routing: Automatic subdomain detection and routing via Next.js middleware
- Self-Hosted: Designed for self-hosting with SQLite database
- Framework: Next.js 16 (App Router)
- Language: TypeScript
- Database: SQLite with Drizzle ORM
- Authentication: JWT tokens with
josepackage - Styling: Tailwind CSS v4
- UI Components: Shadcn UI
- Validation: Zod
- Node.js 18+
- pnpm (recommended) or npm/yarn
- For production: A server with NGINX and SSL certificate support
pnpm installNote: better-sqlite3 is a native module. If you encounter build errors, rebuild it:
pnpm rebuild better-sqlite3Create a .env.local file in the root directory:
DATABASE_URL=./data/sqlite.db
JWT_SECRET=your-secret-key-change-this-in-production
ROOT_DOMAIN=localhost
NODE_ENV=developmentImportant: Generate a strong JWT secret:
openssl rand -base64 32pnpm db:pushThis will create the SQLite database and tables in the data/ directory.
pnpm devOpen http://localhost:3000 in your browser.
- Visit
http://localhost:3000/signup - Fill in your username, display name, email, and password
- After signup, you'll be redirected to your subdomain profile (e.g.,
http://yourusername.localhost:3000)
- Public Profile: Visit
http://yourusername.localhost:3000(or your subdomain in production) - Dashboard: Visit
http://localhost:3000/dashboardto edit your profile
The application supports *.localhost subdomains for local development. Modern browsers automatically resolve these subdomains to localhost.
Example:
- Sign up with username
john - Access profile at
http://john.localhost:3000
src/
├── app/
│ ├── api/
│ │ ├── auth/ # Authentication endpoints
│ │ └── profile/ # Profile management endpoints
│ ├── dashboard/ # Protected dashboard page
│ ├── login/ # Login page
│ ├── profile/[username]/ # Dynamic profile pages
│ ├── signup/ # Signup page
│ └── page.tsx # Landing page
├── components/
│ └── ui/ # Shadcn UI components
├── db/
│ ├── schema.ts # Drizzle schema definitions
│ └── index.ts # Database connection
├── lib/
│ ├── auth.ts # JWT and password utilities
│ ├── session.ts # Session management
│ ├── validations.ts # Zod schemas
│ └── utils.ts # Utility functions
└── middleware.ts # Subdomain detection and routing
pnpm dev- Start development serverpnpm build- Build for productionpnpm start- Start production serverpnpm lint- Run ESLintpnpm typecheck- Run TypeScript type checkingpnpm db:push- Push schema changes to databasepnpm db:generate- Generate migration filespnpm db:migrate- Run migrationspnpm db:studio- Open Drizzle Studio (database GUI)
The application uses SQLite with Drizzle ORM. The database file is stored at ./data/sqlite.db (configurable via DATABASE_URL).
- users: Stores user accounts with username, email, password (hashed), display name, bio, and avatar URL
- JWT tokens stored in httpOnly cookies
- Password hashing with bcryptjs
- Session management via
cookies-next - Protected routes check authentication status
For production deployment instructions, including NGINX configuration, SSL setup, and wildcard subdomain configuration, see docs/DEPLOYMENT.md.
For project planning and roadmap, see docs/PROJECT.md.
The following subdomains are reserved and cannot be used as usernames:
wwwapiadminappmailftp
- Passwords are hashed using bcrypt
- JWT tokens are stored in httpOnly cookies
- Environment variables should be kept secure
- Use strong JWT secrets in production
- Database file permissions should be restricted
This project is private and for personal use.