Infrastructure
SyncAD runs on AWS with Docker-based deployments and GitHub Actions for CI/CD.
AWS Services
| Service | Purpose |
|---|---|
| RDS PostgreSQL | Central DB + per-school DBs. db.t3.medium minimum, Multi-AZ in production |
| S3 | File storage: student photos, announcement attachments, library book covers |
| SES | Transactional email: OTP delivery, fee receipts, announcements |
| SNS | SMS + Push: OTP via SNS → SMS, push notifications → FCM |
| Route53 | DNS management for school subdomains ({slug}.syncad.in) |
| ACM | SSL certificates via Let's Encrypt (wildcard *.syncad.in) |
| ECR | Container registry for API Docker images |
| ECS Fargate | Container hosting for NestJS API (no EC2 management) |
| CloudFront | CDN for static assets (Next.js UIs) |
| ElastiCache Redis | Socket.IO adapter, OTP storage, session cache |
| CodeBuild | CI/CD build pipeline |
| CodeDeploy | Staged deployments to ECS |
Database Connectivity
All PostgreSQL connections use TLS:
-- RDS parameter group
ssl = on
ssl_ca = /rds-combined-ca-bundle.pem
Connections are pooled via pg-pool (max 10 connections per school DB per API instance).
Storage — S3 Presigned URLs
Files are never uploaded through the API directly. Instead:
// 1. Client requests a presigned upload URL
POST /school-admin/upload/presign
{ "fileName": "student-photo.jpg", "contentType": "image/jpeg" }
// Response
{
"uploadUrl": "https://s3.amazonaws.com/bucket/key?X-Amz-Signature=...",
"publicUrl": "https://cdn.syncad.in/students/photo.jpg"
}
// 2. Client uploads directly to S3 using presigned URL
PUT https://s3.amazonaws.com/bucket/key?X-Amz-Signature=...
Content-Type: image/jpeg
<binary data>
Email — SES
// AWS SES for OTP and notifications
await sesClient.sendEmail({
Source: 'noreply@syncad.in',
Destination: { ToAddresses: [email] },
Message: {
Subject: { Data: 'Your SyncAD OTP' },
Body: { Html: { Data: `<p>Your OTP is: <b>${otp}</b></p>` } },
},
});
Push Notifications — SNS → FCM
// Send push notification to parent device
await snsClient.publish({
TargetArn: deviceEndpoint.EndpointArn,
Message: JSON.stringify({
default: 'New announcement from school',
GCM: JSON.stringify({
notification: {
title: 'SyncAD',
body: 'New announcement: Mid-term exams schedule',
click_action: 'OPEN_ACTIVITY',
},
data: { type: 'ANNOUNCEMENT', id: announcementId },
}),
}),
MessageStructure: 'json',
});
Docker
API Dockerfile
# apps/api/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build --filter api
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/api/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3001
CMD ["node", "dist/main.js"]
docker-compose (local dev)
# docker-compose.yml (in syncad root)
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: password
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
postgres_data:
GitHub Actions CI/CD
# .github/workflows/deploy-api.yml
name: Deploy API
on:
push:
branches: [main, dev]
paths: ['apps/api/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 8 }
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build --filter api
- run: pnpm test --filter api
- name: Build and push Docker image
run: |
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REGISTRY
docker build -t $ECR_REGISTRY/syncad-api:$GITHUB_SHA -f apps/api/Dockerfile .
docker push $ECR_REGISTRY/syncad-api:$GITHUB_SHA
- name: Deploy to ECS
run: |
aws ecs update-service --cluster syncad --service api --force-new-deployment
Environment Variables
| Variable | Description | Example |
|---|---|---|
DATABASE_URL | Central DB connection string | postgresql://user:pass@rds:5432/syncad_central |
JWT_SECRET | JWT signing secret (256-bit) | your-secret-here |
JWT_EXPIRES_IN | Access token expiry | 15m |
REFRESH_TOKEN_EXPIRES_IN | Refresh token expiry | 7d |
AWS_ACCESS_KEY_ID | AWS credentials | — |
AWS_SECRET_ACCESS_KEY | AWS credentials | — |
AWS_REGION | AWS region | ap-south-1 |
REDIS_URL | Redis connection | redis://redis:6379 |
SES_FROM_EMAIL | Verified sender email | noreply@syncad.in |
S3_BUCKET | Assets bucket name | syncad-assets |
S3_CDN_URL | CloudFront CDN URL | https://cdn.syncad.in |
Deployment Environments
| Environment | Trigger | URL |
|---|---|---|
local | pnpm dev | localhost:3001 |
dev | push to dev branch | dev-api.metaonus.in |
staging | push to main branch | staging-api.metaonus.in |
production | git tag v*.*.* | api.metaonus.in |