Skip to main content

SCS — School Creation Service

Location: syncad/apps/scs Stack: Go 1.21+ Port: 8080 (dev)

Overview

Automates the full onboarding of a new school: creates an isolated PostgreSQL database, provisions DNS, issues SSL, runs migrations, and registers the school in the central DB.

API

Protected by API key header: X-SCS-Key: <secret>

POST /school
Content-Type: application/json
X-SCS-Key: <admin-key>

{
"name": "Springfield High School",
"slug": "springfield-high",
"district": "Delhi",
"board": "CBSE",
"adminEmail": "admin@springfield.edu",
"phone": "+919876543210"
}

Response:

{
"id": "uuid",
"name": "Springfield High School",
"slug": "springfield-high",
"databaseName": "school_abc123",
"subdomain": "springfield-high.syncad.in",
"status": "provisioning",
"createdAt": "2024-04-01T10:00:00Z"
}
GET /school/:id
DELETE /school/:id   # Tear down: drop DB, delete DNS, revoke SSL

Provisioning Steps

When POST /school is called, the SCS service executes:

StepActionDuration
1Validate slug uniqueness in central DB~100ms
2Create PostgreSQL database school_{uuid}~500ms
3Run Drizzle migrations (50+ tables)~2s
4Create Route53 DNS record {slug}.syncad.in~1s
5Issue Let's Encrypt SSL certificate~3s
6Insert school record into central DB~100ms
7Send welcome email to admin~500ms

Total: ~8 seconds. Client polls GET /school/:id for status updates.

Code Structure

apps/scs/
├── cmd/server/main.go # Entry point
├── internal/
│ ├── provision.go # Orchestrates provisioning steps
│ ├── database.go # PostgreSQL pool management
│ ├── dns.go # Route53 client
│ ├── ssl.go # Let's Encrypt ACM client
│ ├── mail.go # SES welcome email
│ └── handlers/
│ ├── school.go # HTTP handlers
│ └── health.go
├── migrations/
│ └── 001_initial/ # SQL files for school DB schema
└── go.mod

Main Provisioning Logic

// internal/provision.go
func ProvisionSchool(ctx context.Context, req CreateSchoolRequest) (*School, error) {
dbName := fmt.Sprintf("school_%s", uuid.New().String())

// 1. Create isolated DB
if err := s.pgPool.CreateDatabase(ctx, dbName); err != nil {
return nil, fmt.Errorf("create database: %w", err)
}

// 2. Run migrations
schoolPool, err := s.pgPool.GetPool(dbName)
if err != nil {
return nil, fmt.Errorf("get pool: %w", err)
}
if err := s.migrator.Run(schoolPool, s.migrations); err != nil {
return nil, fmt.Errorf("run migrations: %w", err)
}

// 3. Create DNS record
subdomain := fmt.Sprintf("%s.syncad.in", req.Slug)
if err := s.route53.CreateSubdomain(ctx, subdomain); err != nil {
return nil, fmt.Errorf("create dns: %w", err)
}

// 4. Issue SSL
cert, err := s.acm.IssueCertificate(ctx, subdomain)
if err != nil {
return nil, fmt.Errorf("issue cert: %w", err)
}

// 5. Register in central DB
school := &School{
ID: uuid.New().String(),
Name: req.Name,
Slug: req.Slug,
DatabaseName: dbName,
Subdomain: subdomain,
ACMArn: cert,
}
return s.centralDB.CreateSchool(school)
}

Error Handling & Rollback

If any step fails, the service attempts rollback:

func (s *SCS) rollback(ctx context.Context, school *School) {
s.pgPool.DropDatabase(ctx, school.DatabaseName)
s.route53.DeleteRecord(ctx, school.Subdomain)
if school.ACMArn != "" {
s.acm.RevokeCertificate(ctx, school.ACMArn)
}
s.centralDB.DeleteSchool(ctx, school.ID)
}

Environment Variables

VariableDescription
DATABASE_URLCentral PostgreSQL connection string
AWS_ACCESS_KEY_IDAWS credentials
AWS_SECRET_ACCESS_KEYAWS credentials
AWS_REGIONAWS region
ROUTE53_HOSTED_ZONE_IDRoute53 hosted zone ID
SCS_API_KEYAPI key for authenticating provisioning requests
SES_FROM_EMAILSender email for welcome emails