A modern Trello clone with a GraphQL API, a Next.js frontend, and a Postgres database.
- Docker and Docker Compose installed
- Node.js 20+ (for local development without Docker)
- Git
.env.devis used for local development with Docker..env.prodis used for production with Docker..env.dev.exampleand.env.prod.exampleshow the expected variables with placeholder values.
Never commit real .env files to version control. Only the .example files are tracked.
When running the dev compose file, you get:
- Postgres:
localhost:5432 - Backend (GraphQL API):
http://localhost:4000/graphql - Backend Tests (watch): runs unit tests on file change
- Backend Coverage (HTML):
http://localhost:7331 - Frontend (Next.js):
http://localhost:3000 - pgAdmin:
http://localhost:5050 - Docs (SpectaQL):
http://localhost:4400 - Prisma Studio:
http://localhost:5555 - MinIO API:
http://localhost:9000 - MinIO Console:
http://localhost:9001
Prisma Studio is a web UI to browse and edit your database tables during development.
Hot reload is enabled for both backend and frontend in the dev Docker setup.
When you edit files in backend/ or frontend/, the containers will automatically reload.
docker compose -f docker-compose.dev.yml --env-file .env.dev up --buildStart the test watcher:
docker compose -f docker-compose.dev.yml --env-file .env.dev up backend-testsOpen the HTML coverage report:
http://localhost:7331- Files are generated in
backend/coverage/lcov-report.
Stop services:
docker compose -f docker-compose.dev.yml --env-file .env.dev downReset everything (containers + volumes):
docker compose -f docker-compose.dev.yml --env-file .env.dev down -vThe script scripts/clean_rebuild.sh removes containers, volumes, networks, and images for a clean rebuild.
./scripts/clean_rebuild.sh./scripts/clean_rebuild.sh --prodOpen http://localhost:5050 and log in with the values from:
PGADMIN_DEFAULT_EMAILPGADMIN_DEFAULT_PASSWORD
The Postgres server is pre-registered inside pgAdmin.
If you don’t see tables, the database is empty and migrations/seeds have not been run yet.
You can run the services locally if you have Postgres installed.
cd backend
npm install
cat > .env << ENV_EOF
NODE_ENV=development
PORT=4000
DATABASE_URL=postgresql://username:password@localhost:5432/epitrello?schema=public
JWT_SECRET=dev_jwt_secret_change_in_production
CORS_ORIGIN=http://localhost:3000
ENV_EOF
npm run devcd frontend
npm install
cat > .env.local << ENV_EOF
NEXT_PUBLIC_GRAPHQL_API=http://localhost:4000/graphql
ENV_EOF
npm run dev-
Create the file:
cp .env.prod.example .env.prod nano .env.prod
-
Start services:
docker compose -f docker-compose.prod.yml --env-file .env.prod up -d --build
Common variables used by docker-compose.dev.yml:
POSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DBDATABASE_URLJWT_SECRETCORS_ORIGINBACKEND_PORTFRONTEND_PORTNEXT_PUBLIC_GRAPHQL_APIPGADMIN_DEFAULT_EMAILPGADMIN_DEFAULT_PASSWORDMINIO_ROOT_USER(default:minioadmin)MINIO_ROOT_PASSWORD(default:minioadmin)MINIO_PORT(default:9000)MINIO_CONSOLE_PORT(default:9001)MINIO_ENDPOINT(default:localhostfor local dev,miniofor Docker) - Internal endpoint for backend operationsMINIO_EXTERNAL_ENDPOINT(default:localhost) - External endpoint for presigned URLs (used by frontend)MINIO_EXTERNAL_PORT(default:9000) - External port for presigned URLsMINIO_USE_SSL(default:false)MINIO_ACCESS_KEY(default:minioadmin)MINIO_SECRET_KEY(default:minioadmin)MINIO_BUCKET_NAME(default:app-uploads)GOOGLE_CLIENT_ID- Google OAuth Client ID (optional, required for Google OAuth)GOOGLE_CLIENT_SECRET- Google OAuth Client Secret (optional, required for Google OAuth)GITHUB_CLIENT_ID- GitHub OAuth Client ID (optional, required for GitHub OAuth)GITHUB_CLIENT_SECRET- GitHub OAuth Client Secret (optional, required for GitHub OAuth)OAUTH_REDIRECT_URI- OAuth callback URL (default:http://localhost:4000/auth/{provider}/callback)FRONTEND_URL- Frontend URL for OAuth redirects (default:http://localhost:3000)
This usually means the database has not been migrated/seeded yet.
Run the migrations or seeds from the backend (once they exist).
You must run Prisma migrations to create/update database tables after schema changes.
docker compose -f docker-compose.dev.yml --env-file .env.dev exec backend \
npx prisma migrate dev --name initcd backend
export DATABASE_URL="postgresql://user:password@localhost:5432/epitrello?schema=public"
npx prisma migrate dev --name initIf your
DATABASE_URLusespostgres:5432, run the migration inside Docker.
postgresis the Docker service name, not available locally.
Change BACKEND_PORT or FRONTEND_PORT inside .env.dev and restart.
If you see permission errors with volumes:
sudo chown -R $USER:$USER .MinIO is configured as an S3-compatible object storage service for file uploads.
- MinIO API: http://localhost:9000
- MinIO Console: http://localhost:9001
- Default credentials: minioadmin / minioadmin
The app-uploads bucket is automatically created on startup with public download access.
Important: The backend uses two different endpoints:
- Internal endpoint (
MINIO_ENDPOINT): Used for backend operations (delete, copy, move, list, metadata). In Docker, this isminio(the Docker service name). For local dev without Docker, uselocalhost. - External endpoint (
MINIO_EXTERNAL_ENDPOINT): Used for generating presigned URLs that the frontend will use. This should always belocalhost(or your public domain) so the frontend can access MinIO from outside Docker.
This separation ensures that:
- Backend can communicate with MinIO using the Docker service name internally
- Frontend receives presigned URLs pointing to
localhost:9000which it can access from the browser
Both endpoints are automatically configured in docker-compose.dev.yml.
The StorageService provides methods for:
- Generating presigned upload URLs
- Generating presigned download URLs
- Deleting objects
Example:
// Inject StorageService
constructor(private readonly storageService: StorageService) {}
// Generate upload URL
const uploadUrl = await this.storageService.getUploadUrl('path/to/file.jpg', 3600);
// Generate download URL
const downloadUrl = await this.storageService.getDownloadUrl('path/to/file.jpg', 3600);
// Delete object
await this.storageService.deleteObject('path/to/file.jpg');The application supports OAuth authentication via Google and GitHub. OAuth uses REST endpoints while the rest of the API uses GraphQL.
-
Google OAuth Setup:
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable Google+ API
- Go to "Credentials" → "Create Credentials" → "OAuth client ID"
- Choose "Web application"
- Add authorized redirect URI:
http://localhost:4000/auth/google/callback(or your production URL) - Copy the Client ID and Client Secret
-
GitHub OAuth Setup:
- Go to GitHub Developer Settings
- Click "New OAuth App"
- Set Application name
- Set Homepage URL:
http://localhost:3000(or your frontend URL) - Set Authorization callback URL:
http://localhost:4000/auth/github/callback(or your backend URL) - Copy the Client ID and Client Secret
-
Environment Variables: Add these to your
.env.devor.env.prod:GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret OAUTH_REDIRECT_URI=http://localhost:4000/auth/{provider}/callback FRONTEND_URL=http://localhost:3000
- User clicks "Login with Google" or "Login with GitHub"
- Frontend redirects to
GET /auth/googleorGET /auth/github - User authenticates with the provider
- Provider redirects to callback endpoint
- Backend creates/logs in user and generates JWT tokens
- User is redirected to frontend with tokens in URL:
/auth/callback?accessToken=...&refreshToken=... - Frontend stores tokens and user is logged in
OAuth accounts are stored in the OAuthAccount table with:
provider: 'google' or 'github'providerUserId: Unique ID from the OAuth provideruserId: Foreign key to User table- Unique constraint on
(provider, providerUserId)
Users created via OAuth have passwordHash set to null.
GET /auth/google- Redirects to Google OAuthGET /auth/google/callback- Google OAuth callbackGET /auth/github- Redirects to GitHub OAuthGET /auth/github/callback- GitHub OAuth callback
See POSTMAN_OAUTH.md for detailed Postman testing instructions.
- Read docs/README.md
- Start building features!