Express.js REST API for Compass, running at https://api.compassmeet.com.
The API handles:
- User authentication and management
- Profile CRUD operations
- Search and filtering
- Messaging
- Notifications
- Compatibility scoring
- Events management
- WebSocket connections for real-time features
- Runtime: Node.js 20+
- Framework: Express.js 5.0
- Language: TypeScript
- Database: PostgreSQL (via Supabase)
- ORM: pg-promise
- Validation: Zod
- WebSocket: ws library
- API Docs: Swagger/OpenAPI
backend/api/
├── src/
│ ├── app.ts # Express app setup
│ ├── routes.ts # Route definitions
│ ├── test.ts # Test utilities
│ ├── get-*.ts # GET endpoints
│ ├── create-*.ts # POST endpoints
│ ├── update-*.ts # PUT/PATCH endpoints
│ ├── delete-*.ts # DELETE endpoints
│ └── helpers/ # Shared utilities
├── tests/
│ └── unit/ # Unit tests
├── package.json
├── tsconfig.json
└── README.md
- Node.js 20.x or later
- Yarn
- Access to Supabase project (for database)
# From root directory
yarn installYou must also have the gcloud CLI.
On macOS:
brew install --cask google-cloud-sdkOn Linux:
sudo apt-get update && sudo apt-get install google-cloud-sdkThen:
gcloud init
gcloud auth login
gcloud config set project YOUR_PROJECT_IDYou also need opentofu and docker. Try running this (from root) on Linux or macOS for a faster install:
./script/setup.shIf it doesn't work, you can install them manually (google how to install opentofu and docker for your OS).
# Run all services (web + API)
yarn dev
# Run API only (from backend/api)
cd backend/api
yarn serveThe API runs on http://localhost:8088 when running locally with the full stack.
# Run unit tests
yarn test
# Run with coverage
yarn test --coverage# Check lint
yarn lint
# Fix issues
yarn lint-fix| Method | Endpoint | Description |
|---|---|---|
| POST | /create-user |
Create new user |
| Method | Endpoint | Description |
|---|---|---|
| GET | /get-me |
Get current user |
| PUT | /update-me |
Update current user |
| DELETE | /delete-me |
Delete account |
| Method | Endpoint | Description |
|---|---|---|
| GET | /get-profiles |
List profiles |
| GET | /get-profile |
Get single profile |
| POST | /create-profile |
Create profile |
| PUT | /update-profile |
Update profile |
| DELETE | /delete-profile |
Delete profile |
| Method | Endpoint | Description |
|---|---|---|
| GET | /get-private-messages |
Get messages |
| POST | /create-private-user-message |
Send message |
| PUT | /edit-message |
Edit message |
| DELETE | /delete-message |
Delete message |
| Method | Endpoint | Description |
|---|---|---|
| GET | /get-notifications |
List notifications |
| PUT | /update-notif-setting |
Update settings |
| Method | Endpoint | Description |
|---|---|---|
| GET | /search-users |
Search users |
| GET | /search-location |
Search by location |
| Method | Endpoint | Description |
|---|---|---|
| GET | /get-compatibility-questions |
List questions |
| POST | /set-compatibility-answers |
Submit answers |
| GET | /compatible-profiles |
Get compatible profiles |
Add endpoint definition in common/src/api/schema.ts:
const endpoints = {
myEndpoint: {
method: 'POST',
authed: true,
returns: z.object({
success: z.boolean(),
data: z.any(),
}),
props: z
.object({
userId: z.string(),
option: z.string().optional(),
})
.strict(),
},
}Create handler file in backend/api/src/:
import {z} from 'zod'
import {APIHandler} from './helpers/endpoint'
export const myEndpoint: APIHandler<'myEndpoint'> = async (props, auth) => {
const {userId, option} = props
// Implementation
return {
success: true,
data: {userId},
}
}Add to routes.ts:
import {myEndpoint} from './my-endpoint'
const handlers = {
myEndpoint,
// ...
}Use the authed: true schema property. The auth object is passed to the handler:
export const getProfile: APIHandler<'get-profile'> = async (props, auth) => {
// auth.uid - user ID
// auth.creds - credentials type
}firebase- Firebase Auth tokensession- Session-based auth
import {createSupabaseDirectClient} from 'shared/supabase/init'
const pg = createSupabaseDirectClient()
const result = await pg.oneOrNone<User>('SELECT * FROM users WHERE id = $1', [userId])But this works only in the front-end.
import {db} from 'web/lib/supabase/db'
const {data, error} = await db.from('profiles').select('*').eq('user_id', userId)The API includes built-in rate limiting:
export const myEndpoint: APIHandler<'myEndpoint'> = withRateLimit(
async (props, auth) => {
// Handler implementation
},
{
name: 'my-endpoint',
limit: 100,
windowMs: 60 * 1000, // 1 minute
},
)Use APIError for consistent error responses:
import {APIError} from './helpers/endpoint'
throw APIError(404, 'User not found')
throw APIError(400, 'Invalid input', {field: 'email'})Error codes:
400- Bad Request401- Unauthorized403- Forbidden404- Not Found429- Too Many Requests500- Internal Server Error
WebSocket connections are handled for real-time features:
// Subscribe to updates
ws.subscribe('user/123', (data) => {
console.log('User updated:', data)
})
// Unsubscribe
ws.unsubscribe('user/123', callback)Available topics:
user/{userId}- User updatesprivate-user/{userId}- Private user updatesmessage/{channelId}- New messages
Use the shared logger:
import {log} from 'shared/monitoring/log'
log.info('Processing request', {userId: auth.uid})
log.error('Failed to process', error)Deployments are automated via GitHub Actions. Push to main triggers deployment:
# Increment version
# Update package.json version
git add package.json
git commit -m "chore: bump version"
git push origin maincd backend/api
./deploy-api.sh prodRun in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs, files, debug, etc.
# SSH into production server
cd backend/api
./ssh-api.sh prodUseful commands on server:
sudo journalctl -u konlet-startup --no-pager -ef # View logs
sudo docker logs -f $(sudo docker ps -alq) # Container logs
docker exec -it $(sudo docker ps -alq) sh # Shell access
docker run -it --rm $(docker images -q | head -n 1) sh
docker rmi -f $(docker images -aq)Required secrets (set in Google Cloud Secrets Manager):
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
FIREBASE_PROJECT_ID |
Firebase project ID |
FIREBASE_PRIVATE_KEY |
Firebase private key |
SUPABASE_SERVICE_KEY |
Supabase service role key |
JWT_SECRET |
JWT signing secret |
// tests/unit/my-endpoint.unit.test.ts
import {myEndpoint} from '../my-endpoint'
describe('myEndpoint', () => {
it('should return success', async () => {
const result = await myEndpoint({userId: '123'}, mockAuth)
expect(result.success).toBe(true)
})
})const mockPg = {
oneOrNone: jest.fn().mockResolvedValue({id: '123'}),
}Full API docs available at:
- Production: https://api.compassmeet.com
- Local: http://localhost:8088 (when running)
Docs are generated from route definitions in app.ts.
This section is only for the people who are creating a server from scratch, for instance for a forked project.
One-time commands you may need to run:
gcloud artifacts repositories create builds \
--repository-format=docker \
--location=us-west1 \
--description="Docker images for API"
gcloud auth configure-docker us-west1-docker.pkg.dev
gcloud config set project compass-130ba
gcloud projects add-iam-policy-binding compass-130ba \
--member="user:YOUR_EMAIL@gmail.com" \
--role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding compass-130ba \
--member="user:YOUR_EMAIL@gmail.com" \
--role="roles/storage.objectAdmin"
gsutil mb -l us-west1 gs://compass-130ba-terraform-state
gsutil uniformbucketlevelaccess set on gs://compass-130ba-terraform-state
gsutil iam ch user:YOUR_EMAIL@gmail.com:roles/storage.admin gs://compass-130ba-terraform-state
tofu init
gcloud projects add-iam-policy-binding compass-130ba \
--member="serviceAccount:253367029065-compute@developer.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud run services list
gcloud compute backend-services update api-backend \
--global \
--timeout=600sSet up the saved search notifications job:
gcloud scheduler jobs create http daily-saved-search-notifications \
--schedule="0 16 * * *" \
--uri="https://api.compassmeet.com/internal/send-search-notifications" \
--http-method=POST \
--headers="x-api-key=<API_KEY>" \
--time-zone="UTC" \
--location=us-west1View it here.
gcloud iam service-accounts create ci-deployer \
--display-name="CI Deployer"
gcloud projects add-iam-policy-binding compass-130ba \
--member="serviceAccount:ci-deployer@compass-130ba.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding compass-130ba \
--member="serviceAccount:ci-deployer@compass-130ba.iam.gserviceaccount.com" \
--role="roles/storage.objectAdmin"
gcloud projects add-iam-policy-binding compass-130ba \
--member="serviceAccount:ci-deployer@compass-130ba.iam.gserviceaccount.com" \
--role="roles/storage.admin"
gcloud projects add-iam-policy-binding compass-130ba \
--member="serviceAccount:ci-deployer@compass-130ba.iam.gserviceaccount.com" \
--role="roles/compute.admin"
gcloud iam service-accounts add-iam-policy-binding \
253367029065-compute@developer.gserviceaccount.com \
--member="serviceAccount:ci-deployer@compass-130ba.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"
gcloud iam service-accounts keys create keyfile.json --iam-account=ci-deployer@compass-130ba.iam.gserviceaccount.com- After deployment, Terraform assigns a static external IP to this resource.
- You can get it manually:
gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)"
34.117.20.215Since Vercel manages your domain (compassmeet.com):
- Log in to Vercel dashboard.
- Go to Domains → compassmeet.com → Add Record.
- Add an A record for your API subdomain:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | api | 34.123.45.67 | 600 s |
Nameis just the subdomain:api→api.compassmeet.com.Valueis the external IP of the LB from step 1.
Verify connectivity From your local machine:
nslookup api.compassmeet.com
ping -c 3 api.compassmeet.com
curl -I https://api.compassmeet.comnslookupshould return the LB IP (34.123.45.67).curl -Ishould return200 OKfrom your service.
If SSL isn’t ready (may take 15 mins), check LB logs:
gcloud compute ssl-certificates describe api-lb-cert-2Secrets are strings that shouldn't be checked into Git (eg API keys, passwords).
Add the secrets for your specific project in Google Cloud Secrets manager, so that the virtual machine can access them.
For Compass, the name of the secrets are in secrets.ts.