A TypeScript backend for tracking Diablo 2 unique item collections (Holy Grail). Built with Fastify, Drizzle ORM, and PostgreSQL.
- Authentication: OAuth social login (Google/Discord) with session management
- Item Tracking: Mark Diablo 2 unique items, set items, and runewords as found/unfound
- Public API: Browse all items without authentication
- Account Linking: Automatically links accounts when using the same email across providers
- Type Safety: Full TypeScript support with shared type definitions
- Runtime: Node.js with TypeScript
- Framework: Fastify
- Database: PostgreSQL (via Docker)
- ORM: Drizzle ORM
- Auth: OAuth 2.0 with Google and Discord providers using @oslojs/oauth2
- Dev Tools: ESLint, Prettier, tsx
- API Testing: Bruno (collection included in
bruno/directory)
- Node.js 18+
- pnpm
- Docker Desktop
- Google OAuth app (Google Cloud Console)
- Discord OAuth app (Discord Developer Portal)
-
Clone and install dependencies:
git clone <repo-url> cd holy-grail-server pnpm install
-
Set up environment variables:
cp .env.example .env
Update
.envwith your OAuth credentials and configuration. -
Set up OAuth applications:
Google:
- Go to Google Cloud Console
- Create OAuth 2.0 Client ID
- Add authorized origins and redirect URIs
Discord:
- Go to Discord Developer Portal
- Create new application
- Set up OAuth2 redirect URIs
-
Start the database:
pnpm db:start
This starts a PostgreSQL container via Docker.
-
Run database migrations:
pnpm db:generate pnpm db:migrate
-
Start the development server:
pnpm dev
The server will be running at http://localhost:3000.
The project uses PostgreSQL running in Docker for easy setup:
- Container name:
d2-postgres - Database:
d2_holy_grail - Port:
5432 - Credentials:
postgres/password(local development only)
If you don't have the database container yet, create it:
docker run --name d2-postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=d2_holy_grail -p 5432:5432 -d postgres:15pnpm db:start # Start PostgreSQL container
pnpm db:stop # Stop PostgreSQL container
pnpm db:restart # Restart PostgreSQL container
pnpm db:logs # View database logs
pnpm db:generate # Generate new migrations
pnpm db:migrate # Apply migrations to databaseGET /health- Health checkGET /items?types=uniqueItems,setItems,runes,baseItems- Get items by type (requires types query parameter)GET /items/:itemKey- Get specific item by keyGET /runewords- Get all runewordsGET /runewords/:runewordKey- Get specific runeword by key
GET /auth/me- Get current user informationGET /auth/google- Initiate Google OAuth loginGET /auth/discord- Initiate Discord OAuth loginGET /auth/google/callback- Google OAuth callback (handled automatically)GET /auth/discord/callback- Discord OAuth callback (handled automatically)POST /auth/logout- Logout user
GET /user-items- Get all user's found itemsPOST /user-items/set- Mark single item as found/unfoundPOST /user-items/set-bulk- Bulk import multiple itemsDELETE /user-items/clear- Clear all user items
- Go to Google Cloud Console
- Create a new project or select existing
- Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client ID"
- Set application type to "Web application"
- Add authorized JavaScript origins:
http://localhost:5173(development)https://yourdomain.com(production)
- Add authorized redirect URIs:
http://localhost:3000/auth/google/callback(development)https://yourdomain.com/auth/google/callback(production)
- Go to Discord Developer Portal
- Click "New Application"
- Go to "OAuth2" tab
- Add redirect URIs:
http://localhost:3000/auth/discord/callback(development)https://yourdomain.com/auth/discord/callback(production)
- Copy Client ID and Client Secret
For production deployments with separate frontend/API domains, session cookies are configured with a shared domain:
// In OAuth callback routes
reply.setCookie("session", sessionId, {
domain: ".yourdomain.com", // Allows sharing between subdomains
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});This enables:
- Frontend (
app.yourdomain.com) to access cookies set by API (api.yourdomain.com) - Seamless authentication flow after OAuth redirects
- Proper session persistence across subdomain boundaries
When using CloudFront, ensure OAuth endpoints use policies that forward:
- All query parameters (for OAuth state validation)
- All cookies (for session management)
See HOSTING.md for detailed CloudFront configuration requirements.
The project includes Bruno API tests in the bruno/ directory. Install Bruno and open the collection to test all endpoints.
For testing authenticated endpoints, log in through your browser first and copy the session cookie to Bruno.
Example:
- domain: localhost
- path: /
- key: session
- value:
<id> - Tick "HTTP Only"
pnpm dev # Start development server with hot reload
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint
pnpm lint:fix # Fix ESLint issues
pnpm format # Format code with Prettier
pnpm format:check # Check code formattingsrc/
├── db/
│ ├── index.ts # Database connection
│ └── schema.ts # Database schema definitions
├── lib/
│ ├── auth.ts # Session management utilities
│ └── oauth.ts # OAuth URL generators
├── middleware/
│ └── auth.ts # Authentication middleware
├── routes/
│ ├── auth.ts # OAuth authentication routes
│ ├── items.ts # Item routes
│ └── userItems.ts # User item tracking routes
├── types/
│ └── items.ts # TypeScript type definitions
├── data/
│ └── items.ts # Static item data
└── server.ts # Main server file
For production deployment:
- Update environment variables for production URLs
- Use a managed PostgreSQL service instead of Docker
- Configure OAuth apps with production URLs
- Build the project:
pnpm build - Start with:
pnpm start
- Follow the existing code style (ESLint + Prettier)
- Add types for new features
- Update API tests in Bruno
- Test locally before submitting PRs