Production-ready full-stack boilerplate with authentication, user management, and OTP support.
- Email/password authentication
- OTP (One-Time Password) email-based authentication
- JWT with refresh token rotation
- Email verification
- Password reset flow
- Role-based access control (USER, ADMIN, MODERATOR, SUPER_ADMIN, MANAGER, SUPPORT)
- Permission-based access control
- User registration with admin approval workflow
- User profile management (name, email, phone)
- Admin dashboard with statistics and analytics
- Audit logging system
- User status management (PENDING_APPROVAL, FRESHLY_CREATED_REQUIRES_PASSWORD, ACTIVE, SUSPENDED, BLOCKED, REJECTED, DELETED)
- Bulk operations (OTP enable/disable)
- User growth analytics with charts
- Radix UI components via @arstoien/shared-ui
- Internationalization (Czech, English, Slovak)
- Currency selection (backend only)
- Responsive design
- GraphQL API with subscriptions
- File uploads to S3/MinIO
- Email notifications with Handlebars templates
- Real-time admin notifications
- Effect-TS for functional error handling
- NestJS - Node.js framework
- Prisma - ORM for PostgreSQL
- Apollo GraphQL - API layer
- Redis - Caching and pub/sub
- Passport.js - Authentication
- Nodemailer - Email service
- MinIO/S3 - File storage
- React 19 - UI framework
- Vite - Build tool
- TanStack Router - File-based routing
- Apollo Client - GraphQL client
- Tailwind CSS - Styling
- @arstoien/shared-ui - Component library
- @arstoien/former - Form library
- React Hook Form - Form handling
- Zustand - State management
- i18next - Internationalization
- react-hot-toast - Notifications
- Recharts - Charts
- PostgreSQL 15 - Primary database
- Redis 7 - Cache and sessions
- Docker - Containerization
- pnpm - Package management
- TypeScript - Type safety
arstoien-blrplt/
├── packages/
│ ├── server/ # NestJS backend
│ ├── client/ # React client app
│ └── admin/ # React admin panel
├── docker-compose.yml # Development services
├── package.json # Root workspace config
└── init.sql # Database initialization
- Node.js 22+
- pnpm 9.14.2
- Docker & Docker Compose
- PostgreSQL 15 (or use Docker)
- Redis 7 (or use Docker)
- Clone the repository
git clone https://github.com/arstoien/arstoien-blrplt.git
cd arstoien-blrplt- Install pnpm
corepack enable
corepack prepare pnpm@9.14.2 --activate
# Or install globally
npm install -g pnpm- Install dependencies
pnpm install- Start Docker services
docker-compose up -d- Setup environment variables
# Copy example files
cp packages/server/.env.example packages/server/.env
cp packages/client/.env.example packages/client/.env
cp packages/admin/.env.example packages/admin/.env
# Edit .env files with your configuration- Setup database
# Generate Prisma client
pnpm --filter @blrplt/server prisma generate
# Run migrations
pnpm --filter @blrplt/server prisma migrate dev
# Seed database (optional)
pnpm --filter @blrplt/server prisma db seed- Start development servers
# Start all services
pnpm dev
# Or start individually:
pnpm --filter @blrplt/server dev # Backend on :4000
pnpm --filter @blrplt/client dev # Client on :3000
pnpm --filter @blrplt/admin dev # Admin on :5173# Database
DATABASE_URL="postgresql://blrplt_user:blrplt_password@localhost:5432/blrplt_db"
# Redis
REDIS_URL="redis://localhost:6379"
# JWT
JWT_SECRET="your-jwt-secret"
JWT_REFRESH_SECRET="your-refresh-secret"
JWT_EXPIRES_IN="15m"
JWT_REFRESH_EXPIRES_IN="90d"
# Email (Nodemailer)
SMTP_HOST="localhost"
SMTP_PORT="1025"
SMTP_SECURE="false"
SMTP_USER="" # Optional
SMTP_PASS="" # Optional
EMAIL_FROM="noreply@boilerplate.local"
# S3/MinIO (Optional)
S3_ENDPOINT="http://localhost:9000"
S3_ACCESS_KEY="minioadmin"
S3_SECRET_KEY="minioadmin"
S3_BUCKET="uploads"
S3_REGION="us-east-1"
# Admin Seed Data
ADMIN_EMAIL="admin@boilerplate.local"
ADMIN_PASSWORD="Admin123!"
ADMIN_FIRST_NAME="Admin"
ADMIN_LAST_NAME="User"
# System Settings
SUPPORT_EMAIL="support@boilerplate.local"
OTP_AUTH_ENABLED="true"VITE_API_URL=http://localhost:4000
VITE_GRAPHQL_URL=http://localhost:4000/graphql
VITE_WS_URL=ws://localhost:4000/graphqlpnpm dev- Start all services in developmentpnpm build- Build all packagespnpm test- Run all testspnpm typecheck- TypeScript checkingpnpm lint:check- Check lintingpnpm lint:fix- Fix linting issuespnpm prettier:check- Check formattingpnpm prettier:fix- Fix formatting
pnpm db:migrate- Run migrationspnpm db:push- Push schema changespnpm db:seed- Seed databasepnpm db:studio- Open Prisma Studiopnpm create:admin- Create admin user (requires env vars)pnpm create:test-users- Create test users for development
pnpm codegen- Generate GraphQL typespnpm i18n- Extract i18n strings
- AWS CLI configured with proper credentials
- Database connection details (from Terraform outputs)
- Admin credentials ready
For security, production RDS databases are typically not publicly accessible. To connect:
# 1. Temporarily make RDS publicly accessible
aws rds modify-db-instance \
--db-instance-identifier blrplt-postgres \
--publicly-accessible \
--apply-immediately \
--region eu-central-1
# 2. Wait for the modification to complete
aws rds wait db-instance-available \
--db-instance-identifier blrplt-postgres \
--region eu-central-1
# 3. Add your IP to the security group
MY_IP=$(curl -s ifconfig.me)
echo "Your IP: $MY_IP"
aws ec2 authorize-security-group-ingress \
--group-id <your-security-group-id> \
--protocol tcp \
--port 5432 \
--cidr $MY_IP/32 \
--region eu-central-1# Export database URL (get from Terraform outputs or AWS console)
export DATABASE_URL="postgresql://username:password@host:5432/database"
# Test connection with psql
psql $DATABASE_URL -c "SELECT version();"
# Or check via the application
cd packages/server
pnpm db:studio # Opens Prisma Studio to view datacd packages/server
# Generate Prisma client
pnpm db:generate
# Run production migrations
pnpm db:migrate:prodThe application requires an admin user to manage the system. Create one using environment variables:
cd packages/server
# Set required environment variables
export DATABASE_URL="postgresql://username:password@host:5432/database"
export ADMIN_EMAIL="admin@yourdomain.com"
export ADMIN_PASSWORD="YourSecurePassword123!"
export ADMIN_FIRST_NAME="Admin" # Optional
export ADMIN_LAST_NAME="User" # Optional
# Create the admin user
pnpm create:adminFor development/staging environments, you can create test users:
cd packages/server
# Set required environment variables
export DATABASE_URL="postgresql://username:password@host:5432/database"
export BASE_PWD="YourBasePassword" # Required - base password for test users
# This creates several test accounts with different roles
pnpm create:test-usersTest users created (passwords are BASE_PWD + suffix):
moderator@example.com/${BASE_PWD}_moderator123!(MODERATOR)john.doe@example.com/${BASE_PWD}_user123!(USER)jane.smith@example.com/${BASE_PWD}_user123!(USER)test.user@example.com/${BASE_PWD}_test123!(USER)demo@example.com/${BASE_PWD}_demo123!(USER)
create:test-users in production!
After setup, always revert the security changes:
# 1. Make RDS private again
aws rds modify-db-instance \
--db-instance-identifier blrplt-postgres \
--no-publicly-accessible \
--apply-immediately \
--region eu-central-1
# 2. Remove your IP from security group
aws ec2 revoke-security-group-ingress \
--group-id <your-security-group-id> \
--protocol tcp \
--port 5432 \
--cidr $MY_IP/32 \
--region eu-central-1If direct connection is not possible, you can run commands through your App Runner service:
# Update App Runner start command temporarily
aws apprunner update-service \
--service-arn <your-app-runner-arn> \
--region eu-central-1 \
--source-configuration '{
"ImageRepository": {
"ImageConfiguration": {
"StartCommand": "sh -c \"npx prisma migrate deploy && node src/scripts/create-admin.js && npm start\""
}
}
}'- Make changes to code
- Run type checking:
pnpm typecheck - Run linting:
pnpm lint:check - Run tests:
pnpm test - Generate types (if GraphQL changed):
pnpm codegen - Run migrations (if schema changed):
pnpm db:migrate
- User registers with email + personal info (no password yet)
- User status:
PENDING_APPROVAL - Admin receives notification
- Admin approves/rejects user
- On approval:
- User status →
FRESHLY_CREATED_REQUIRES_PASSWORD - Verification email sent with password setup link
- User status →
- User sets password via email link
- User status →
ACTIVE - User can now login
- User enters email
- System checks if OTP enabled for user
- If OTP disabled: password field shown → traditional login
- If OTP enabled: redirected to OTP flow
- User enters email
- System checks if OTP enabled (system-wide + per-user)
- 6-digit code sent via email (5-minute expiry)
- User enters OTP code
- Code verified → user logged in
- Email automatically verified on successful OTP login
- Rate limited: 3 attempts per 15 minutes
- User clicks "Forgot Password"
- Enters email → reset link sent
- Token valid for 30 minutes
- User sets new password via link
- Can login with new password
# Build images
docker build -t blrplt-server ./packages/server
docker build -t blrplt-client ./packages/client
docker build -t blrplt-admin ./packages/admin
# Run with docker-compose
docker-compose -f docker-compose.production.yml up# Build all packages
yarn build
# Server
cd packages/server
node dist/main.js
# Client (serve static files)
cd packages/client
npx serve -s dist
# Admin (serve static files)
cd packages/admin
npx serve -s distGraphQL Playground available at: http://localhost:4000/graphql
query CurrentUser {
currentUser {
id
email
firstName
lastName
role
status
emailVerifiedAt
otpAuthEnabled
}
}
query IsOtpEnabled($email: String!) {
isOtpEnabled(email: $email)
}# Traditional Login
mutation Login($input: LoginInput!) {
login(loginInput: $input) {
accessToken
refreshToken
user {
id
email
firstName
lastName
}
}
}
# Register (No Password)
mutation Register($input: RegisterInput!) {
register(registerInput: $input) {
user {
id
email
status
}
}
}
# OTP Flow
mutation RequestOtpLogin($email: String!) {
requestOtpLogin(email: $email) {
success
message
}
}
mutation VerifyOtpLogin($input: VerifyOtpInput!) {
verifyOtpLogin(input: $input) {
accessToken
refreshToken
user {
id
email
}
}
}
# Password Management
mutation SetPasswordWithToken($input: SetPasswordWithTokenInput!) {
setPasswordWithToken(input: $input) {
success
message
}
}
mutation ForgotPassword($email: String!) {
forgotPassword(email: $email) {
success
message
}
}
mutation ResetPassword($input: ResetPasswordInput!) {
resetPassword(input: $input) {
success
message
}
}query PendingUsers {
pendingUsers {
id
email
firstName
lastName
createdAt
}
}
query Users($filters: UserFiltersInput, $pagination: PaginationInput) {
users(filters: $filters, pagination: $pagination) {
users {
id
email
role
status
}
total
page
limit
}
}
query AdminStatistics {
adminStatistics {
pendingUsersCount
todayRegistrationsCount
totalUsersCount
}
}
query UserGrowthStats($months: Int) {
userGrowthStats(months: $months) {
period
totalUsers
activeUsers
newUsers
pendingUsers
}
}
query AuditLogs($pagination: PaginationInput) {
auditLogs(pagination: $pagination) {
logs {
id
action
entityType
userId
ipAddress
createdAt
}
total
}
}mutation ApproveUser($id: String!) {
approveUser(id: $id) {
id
status
}
}
mutation RejectUser($id: String!, $reason: String!) {
rejectUser(id: $id, reason: $reason) {
id
status
}
}
mutation UpdateUser($id: String!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
email
role
status
}
}
mutation BulkUpdateOtp($input: BulkUpdateOtpInput!) {
bulkUpdateOtp(input: $input) {
updated
success
message
}
}
mutation DeleteUser($id: String!, $permanent: Boolean) {
deleteUser(id: $id, permanent: $permanent) {
success
message
}
}subscription AdminPendingCountsChanged {
adminPendingCountsChanged {
pendingUsersCount
pendingApprovalsCount
}
}- @arstoien/devtools - ESLint and Prettier configurations
- @arstoien/shared-ui - Component library (Radix UI + Tailwind)
- @arstoien/former - Form library with dynamic forms
- Fork the repository
- Create your feature branch
- Commit your changes
- Push to the branch
- Open a Pull Request
MIT
For issues and questions, please use the GitHub issues page.
Built with inspiration from best practices in modern full-stack development.