Files
Compass/backend/api/README.md
MartinBraquet 8c68312597 Clean docs
2026-03-01 06:43:10 +01:00

563 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backend API
Express.js REST API for Compass, running at https://api.compassmeet.com.
## Overview
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
## Tech Stack
- **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
## Project Structure
```
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
```
## Getting Started
### Prerequisites
- Node.js 20.x or later
- Yarn
- Access to Supabase project (for database)
### Installation
```bash
# From root directory
yarn install
```
You must also have the `gcloud` CLI.
On macOS:
```bash
brew install --cask google-cloud-sdk
```
On Linux:
```bash
sudo apt-get update && sudo apt-get install google-cloud-sdk
```
Then:
```bash
gcloud init
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
```
You also need `opentofu` and `docker`. Try running this (from root) on Linux or macOS for a faster install:
```bash
./script/setup.sh
```
If it doesn't work, you can install them manually (google how to install `opentofu` and `docker` for your OS).
### Running Locally
```bash
# Run all services (web + API)
yarn dev
# Run API only (from backend/api)
cd backend/api
yarn serve
```
The API runs on http://localhost:8088 when running locally with the full stack.
### Testing
```bash
# Run unit tests
yarn test
# Run with coverage
yarn test --coverage
```
### Linting
```bash
# Check lint
yarn lint
# Fix issues
yarn lint-fix
```
## API Endpoints
### Authentication
| Method | Endpoint | Description |
| ------ | -------------- | --------------- |
| POST | `/create-user` | Create new user |
### Users
| Method | Endpoint | Description |
| ------ | ------------ | ------------------- |
| GET | `/get-me` | Get current user |
| PUT | `/update-me` | Update current user |
| DELETE | `/delete-me` | Delete account |
### Profiles
| 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 |
### Messaging
| 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 |
### Notifications
| Method | Endpoint | Description |
| ------ | ----------------------- | ------------------ |
| GET | `/get-notifications` | List notifications |
| PUT | `/update-notif-setting` | Update settings |
### Search
| Method | Endpoint | Description |
| ------ | ------------------ | ------------------ |
| GET | `/search-users` | Search users |
| GET | `/search-location` | Search by location |
### Compatibility
| Method | Endpoint | Description |
| ------ | ------------------------------ | ----------------------- |
| GET | `/get-compatibility-questions` | List questions |
| POST | `/set-compatibility-answers` | Submit answers |
| GET | `/compatible-profiles` | Get compatible profiles |
## Writing Endpoints
### 1. Define Schema
Add endpoint definition in `common/src/api/schema.ts`:
```typescript
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(),
},
}
```
### 2. Implement Handler
Create handler file in `backend/api/src/`:
```typescript
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},
}
}
```
### 3. Register Route
Add to `routes.ts`:
```typescript
import {myEndpoint} from './my-endpoint'
const handlers = {
myEndpoint,
// ...
}
```
## Authentication
### Authenticated Endpoints
Use the `authed: true` schema property. The auth object is passed to the handler:
```typescript
export const getProfile: APIHandler<'get-profile'> = async (props, auth) => {
// auth.uid - user ID
// auth.creds - credentials type
}
```
### Auth Types
- `firebase` - Firebase Auth token
- `session` - Session-based auth
## Database Access
### Using pg-promise
```typescript
import {createSupabaseDirectClient} from 'shared/supabase/init'
const pg = createSupabaseDirectClient()
const result = await pg.oneOrNone<User>('SELECT * FROM users WHERE id = $1', [userId])
```
### Using Supabase Client
But this works only in the front-end.
```typescript
import {db} from 'web/lib/supabase/db'
const {data, error} = await db.from('profiles').select('*').eq('user_id', userId)
```
## Rate Limiting
The API includes built-in rate limiting:
```typescript
export const myEndpoint: APIHandler<'myEndpoint'> = withRateLimit(
async (props, auth) => {
// Handler implementation
},
{
name: 'my-endpoint',
limit: 100,
windowMs: 60 * 1000, // 1 minute
},
)
```
## Error Handling
Use `APIError` for consistent error responses:
```typescript
import {APIError} from './helpers/endpoint'
throw APIError(404, 'User not found')
throw APIError(400, 'Invalid input', {field: 'email'})
```
Error codes:
- `400` - Bad Request
- `401` - Unauthorized
- `403` - Forbidden
- `404` - Not Found
- `429` - Too Many Requests
- `500` - Internal Server Error
## WebSocket
WebSocket connections are handled for real-time features:
```typescript
// Subscribe to updates
ws.subscribe('user/123', (data) => {
console.log('User updated:', data)
})
// Unsubscribe
ws.unsubscribe('user/123', callback)
```
Available topics:
- `user/{userId}` - User updates
- `private-user/{userId}` - Private user updates
- `message/{channelId}` - New messages
## Logging
Use the shared logger:
```typescript
import {log} from 'shared/monitoring/log'
log.info('Processing request', {userId: auth.uid})
log.error('Failed to process', error)
```
## Deployment
### Production Deployment
Deployments are automated via GitHub Actions. Push to main triggers deployment:
```bash
# Increment version
# Update package.json version
git add package.json
git commit -m "chore: bump version"
git push origin main
```
### Manual Deployment
```bash
cd backend/api
./deploy-api.sh prod
```
### Server Access
Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs,
files, debug, etc.
```bash
# SSH into production server
cd backend/api
./ssh-api.sh prod
```
Useful commands on server:
```bash
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)
```
## Environment Variables
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 |
## Testing
### Writing Unit Tests
```typescript
// 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)
})
})
```
### Mocking Database
```typescript
const mockPg = {
oneOrNone: jest.fn().mockResolvedValue({id: '123'}),
}
```
## API Documentation
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`.
## See Also
- [Main README](../../README.md)
- [Contributing Guide](../../CONTRIBUTING.md)
- [Shared Backend Utils](../shared/README.md)
- [Database Migrations](../../supabase)
### Setup
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:
```bash
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=600s
```
Set up the saved search notifications job:
```bash
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-west1
```
View it [here](https://console.cloud.google.com/cloudscheduler).
##### API Deploy CD
```shell
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
```
##### DNS
- After deployment, Terraform assigns a static external IP to this resource.
- You can get it manually:
```bash
gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)"
34.117.20.215
```
Since Vercel manages your domain (`compassmeet.com`):
1. Log in to [Vercel dashboard](https://vercel.com/dashboard).
2. Go to **Domains → compassmeet.com → Add Record**.
3. Add an **A record** for your API subdomain:
| Type | Name | Value | TTL |
| ---- | ---- | ------------ | ----- |
| A | api | 34.123.45.67 | 600 s |
- `Name` is just the subdomain: `api``api.compassmeet.com`.
- `Value` is the **external IP of the LB** from step 1.
Verify connectivity
From your local machine:
```bash
nslookup api.compassmeet.com
ping -c 3 api.compassmeet.com
curl -I https://api.compassmeet.com
```
- `nslookup` should return the LB IP (`34.123.45.67`).
- `curl -I` should return `200 OK` from your service.
If SSL isnt ready (may take 15 mins), check LB logs:
```bash
gcloud compute ssl-certificates describe api-lb-cert-2
```
##### Secrets management
Secrets 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](https://console.cloud.google.com/security/secret-manager), so that the virtual machine
can access them.
For Compass, the name of the secrets are in [secrets.ts](../../common/src/secrets.ts).