Part of the 100cims monorepo. See the root README for an overview of the full project structure.
The backend API for Cims, built with Next.js 15 and Elysia.
This is a hybrid Next.js + Elysia application:
- Next.js 15 (App Router) serves web pages and provides the runtime
- Elysia 1.4 handles all API routes via a catch-all Next.js route handler
- API is mounted at
/api/*and handled by Elysia - OpenAPI/Swagger documentation auto-generated at
/api/swagger
Elysia is a fast, type-safe TypeScript API framework with excellent OpenAPI support. It provides:
- Automatic type inference from schemas
- Built-in validation with TypeBox
- OpenAPI spec generation
- JWT authentication plugin
- Better performance than traditional Next.js API routes
- Next.js 15 - React framework (App Router)
- Elysia 1.4 - TypeScript API framework
- Drizzle ORM 0.44 - Type-safe SQL toolkit
- PostgreSQL - Primary database
- TypeBox - Runtime type validation
- JWT - Authentication tokens
- AWS S3 - Image storage (avatars, summit photos)
- Google Sheets API - Logging (errors, suggestions, signups)
- Next Intl - Internationalization (en, ca, es)
- Node.js >= 18.0.0
- PostgreSQL database
- AWS S3 bucket (for images)
- Google Cloud service account (for Sheets logging)
From the monorepo root:
# Install dependencies
yarn install
# Set up environment variables
cp packages/api/.env.example packages/api/.env.local
# Edit .env.local with your values
# Start development server
yarn dev:apiThe API will be available at:
- API: http://localhost:3000/api
- Swagger docs: http://localhost:3000/api/swagger
See .env.example for a complete list of required environment variables.
Key variables:
DATABASE_URL- PostgreSQL connection stringAUTH_SECRET- JWT signing secretAWS_*- S3 credentials for image uploadsSHEETS_*- Google service account for logging
This project uses Drizzle ORM with PostgreSQL.
The database includes these main tables:
user- User accounts (Google/Apple OAuth)mountain- Mountain data (peaks, locations, difficulty)summit- User summit logs (photos, dates)plan- Group hiking plansplan_attendee- Plan participantsplan_chat- Plan chat messageschallenge- Curated hiking challengeshiscores- Leaderboard rankingsdonor- App supporters
# From monorepo root
yarn api drizzle-kit push
# Or from this directory
yarn drizzle-kit pushAn initial data script is available at src/db/init-script.sql for populating mountains and challenges.
The API is organized into public and protected routes:
No authentication required:
GET /api/mountains- List all mountainsGET /api/user/:id- Get user profileGET /api/challenge- List challengesGET /api/hiscores- Get leaderboardPOST /api/join- Join waitlist (logs to Google Sheets)
Require JWT authentication (Authorization: Bearer <token>):
POST /api/summit- Log a summitGET /api/summit- Get user's summitsPOST /api/plan- Create a hiking planPOST /api/plan/:id/chat- Send chat messagePOST /api/mountain/:id/image- Upload summit photoPOST /api/user/avatar- Upload avatarPOST /api/donor- Record donation
- User signs in with Google/Apple OAuth (handled by mobile app)
- App sends OAuth token to backend
- Backend validates token and issues JWT
- JWT is used for subsequent authenticated requests
See src/api/routes/@shared/jwt.ts for JWT configuration.
Images are stored in AWS S3:
- Avatar images:
{APP_NAME}/user/avatar/{userId}.jpeg - Summit photos:
{APP_NAME}/mountain/summit/{summitId}.jpeg
See src/api/routes/@shared/s3.ts for S3 client configuration.
The API logs certain events to Google Sheets for analytics:
- Email signups →
[Emails] 2025sheet - Error reports →
[Errors] 2025sheet - User suggestions →
[Suggestions] 2025sheet
Spreadsheet ID (hardcoded): 1FL4Tl4VBnafBtHVRBTwfzhFrPRViVwF_6DE6OxIyCBs
Ensure your service account has editor access to this spreadsheet.
See src/api/lib/sheets.ts for implementation.
Interactive API documentation is available via Swagger UI:
Development: http://localhost:3000/api/swagger
The OpenAPI specification is auto-generated from Elysia route definitions.
src/
├── api/
│ ├── lib/ # Shared utilities
│ │ ├── dates.ts # Date formatting
│ │ ├── images.ts # Image processing
│ │ └── sheets.ts # Google Sheets client
│ ├── routes/
│ │ ├── @shared/ # Shared middleware & utilities
│ │ │ ├── jwt.ts # JWT authentication
│ │ │ ├── s3.ts # S3 client
│ │ │ ├── store.ts # Request context
│ │ │ └── types.ts # Shared types
│ │ ├── public/ # Public API routes
│ │ └── protected/ # Protected API routes (JWT required)
│ ├── schemas/ # TypeBox validation schemas
│ └── routes/index.ts # Elysia app configuration
├── app/ # Next.js App Router pages
│ ├── api/[[...slugs]]/ # Elysia catch-all handler
│ └── ... # Web pages (landing, privacy, etc.)
├── db/
│ ├── schema.ts # Drizzle schema definitions
│ ├── index.ts # Database client
│ └── init-script.sql # Initial data script
└── middleware.ts # Next.js i18n middleware
# Start dev server
yarn dev
# Build for production
yarn build
# Start production server
yarn start
# Lint code
yarn lint
# Generate API types (from Swagger)
yarn generate-api-typesThe API is deployed on Railway from packages/api/Dockerfile.
- Railway builds the Docker image on every push to
main. - Two custom domains are attached to the same service:
cims-sempre-amunt.app(legacy) andfescims.com(primary). Both serve the same Next.js app. - Environment variables must be configured in the Railway service dashboard.
- Health checks + logs + rollbacks are managed from the Railway UI; a previous deployment can be restored with one click.
This project is fully type-safe:
- Database types - Generated from Drizzle schema
- API types - Inferred from Elysia routes and TypeBox schemas
- OpenAPI types - Auto-generated for client consumption
The mobile app consumes OpenAPI types via openapi-typescript.
When adding new API routes:
- Define schema in
src/api/schemas/ - Create route in
src/api/routes/public/orprotected/ - Import and register in
src/api/routes/index.ts - Types and Swagger docs are auto-generated
Made with ❤️ by @jvidalv