Full-stack church management SaaS running on AWS (RDS PostgreSQL, ECS Fargate, S3 + CloudFront).
docker-compose up
This starts PostgreSQL, backend (port 4000), and frontend (port 5173) automatically.
The database is seeded from db/aws_rds_full_schema.sql on first run.
1. Database
Set up a local PostgreSQL instance and run the migration:
psql postgresql://user:pass@localhost:5432/shalom -f db/aws_rds_full_schema.sql
2. Backend
cp .env.example .env
# Edit .env — set DATABASE_URL, JWT_SECRET at minimum
npm install
npm run dev
3. Frontend
cd frontend
cp .env.example .env
# Edit .env — set VITE_API_URL=http://localhost:4000
npm install
npm run dev
.env)| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string |
JWT_SECRET |
✅ | Secret for signing JWT tokens (min 32 chars) |
PORT |
Server port (default: 4000) | |
FRONTEND_URL |
Frontend origin for CORS (default: http://localhost:5173) |
|
GOOGLE_CLIENT_ID |
Google OAuth client ID | |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | |
GOOGLE_REDIRECT_URI |
Google OAuth callback URL | |
OTP_EXPIRY_MINUTES |
OTP validity period (default: 10) | |
AWS_REGION |
AWS region for SNS (default: ap-south-1) |
|
AWS_ACCESS_KEY_ID |
AWS credentials for SNS | |
AWS_SECRET_ACCESS_KEY |
AWS credentials for SNS | |
PAYMENTS_ENABLED |
Set true to enable Razorpay (default: false) |
|
RAZORPAY_KEY_ID |
Razorpay key (required if payments enabled) | |
RAZORPAY_KEY_SECRET |
Razorpay secret (required if payments enabled) | |
SUPER_ADMIN_EMAILS |
Comma-separated super-admin emails |
frontend/.env)| Variable | Required | Description |
|---|---|---|
VITE_API_URL |
✅ | Backend API URL (e.g. http://localhost:4000) |
CloudFront (CDN) ─┬─ S3 (frontend static files)
└─ ALB ─── ECS Fargate (backend container)
│
RDS PostgreSQL
# Set required secrets
export DB_PASSWORD="your-db-password-min-12-chars"
export JWT_SECRET="your-jwt-secret-min-32-chars"
# Run deployment
bash aws/deploy.sh
The script will:
# 1. Deploy infrastructure
aws cloudformation deploy \
--template-file aws/cloudformation.yaml \
--stack-name shalom-stack \
--parameter-overrides DBPassword=<password> JwtSecret=<secret> \
--capabilities CAPABILITY_IAM
# 2. Run database migration (use bastion host or VPN)
psql <DATABASE_URL> -f db/aws_rds_full_schema.sql
# 3. Build & push Docker image
aws ecr get-login-password | docker login --username AWS --password-stdin <ECR_URI>
docker build -t shalom-backend .
docker tag shalom-backend:latest <ECR_URI>:latest
docker push <ECR_URI>:latest
# 4. Build & deploy frontend
cd frontend
VITE_API_URL=<ALB_URL> npm run build
aws s3 sync dist/ s3://<FRONTEND_BUCKET>/ --delete
Two authentication methods:
POST /api/auth/otp/send → POST /api/auth/otp/verify → JWT issuedGET /api/auth/google → Google consent → callback → JWT issuedAll protected routes use Authorization: Bearer <JWT> headers.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/api |
API index |
| Auth | ||
POST |
/api/auth/otp/send |
Send OTP |
POST |
/api/auth/otp/verify |
Verify OTP, get JWT |
GET |
/api/auth/google |
Initiate Google OAuth |
GET |
/api/auth/google/callback |
Google OAuth callback |
POST |
/api/auth/sync-profile |
Sync user profile |
GET |
/api/auth/me |
Get current user |
GET |
/api/auth/member-dashboard |
Member dashboard data |
POST |
/api/auth/update-profile |
Update profile |
POST |
/api/auth/family-members |
Manage family members |
| Members | ||
POST |
/api/members/create |
Create member |
POST |
/api/members/link |
Link member |
GET |
/api/members/list |
List members |
GET |
/api/members/search |
Search members |
GET |
/api/members/:id |
Get member |
PATCH |
/api/members/:id |
Update member |
DELETE |
/api/members/:id |
Delete member |
| Subscriptions | ||
POST |
/api/subscriptions/create |
Create subscription |
GET |
/api/subscriptions/my |
My subscriptions |
POST |
/api/subscriptions/reconcile-overdue |
Reconcile overdue (admin) |
| Payments | ||
GET |
/api/payments/config |
Payment config |
POST |
/api/payments/order |
Create order |
POST |
/api/payments/verify |
Verify payment |
POST |
/api/payments/donation/order |
Donation order |
POST |
/api/payments/donation/verify |
Verify donation |
GET |
/api/payments/:paymentId/receipt |
Get receipt |
| Churches | ||
POST |
/api/churches/create |
Create church |
GET |
/api/churches/summary |
Church summary |
GET |
/api/churches/search |
Search churches |
GET |
/api/churches/id/:id |
Get church |
PATCH |
/api/churches/id/:id |
Update church |
DELETE |
/api/churches/id/:id |
Delete church |
| Pastors | ||
POST |
/api/pastors/create |
Create pastor |
GET |
/api/pastors/list |
List pastors |
GET |
/api/pastors/:id |
Get pastor |
PATCH |
/api/pastors/:id |
Update pastor |
DELETE |
/api/pastors/:id |
Delete pastor |
POST |
/api/pastors/:id/transfer |
Transfer pastor |
| Admins | ||
GET |
/api/admins/list |
List admins |
GET |
/api/admins/search |
Search admins |
POST |
/api/admins/grant |
Grant admin role |
POST |
/api/admins/revoke |
Revoke admin role |
| Engagement | ||
GET/POST |
/api/engagement/events |
Events |
GET/POST |
/api/engagement/notifications |
Notifications |
GET/POST |
/api/engagement/prayer-requests |
Prayer requests |
GET/POST |
/api/announcements/* |
Announcements |
├── aws/ # AWS deployment configs
│ ├── cloudformation.yaml # Full infrastructure template
│ └── deploy.sh # One-command deployment script
├── db/
│ └── aws_rds_full_schema.sql # Combined database migration
├── frontend/ # React 19 + Vite + Tailwind
├── src/
│ ├── config.ts # Environment config
│ ├── app.ts # Express app setup
│ ├── index.ts # Server entry point
│ ├── middleware/
│ │ └── requireAuth.ts # JWT auth middleware
│ ├── routes/ # API route handlers
│ ├── services/
│ │ ├── supabaseClient.ts # PostgreSQL query builder (pg Pool)
│ │ └── *.ts # Business logic services
│ └── types/ # TypeScript types
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Local dev environment
└── package.json
SUPER_ADMIN_EMAILS in .env. Primary super-admin is sonusrujan76@gmail.com.PAYMENTS_ENABLED=false, /api/payments/* returns HTTP 503 by design.supabaseClient.ts provides a Supabase-compatible API over raw pg Pool..env, set:
PAYMENTS_ENABLED=trueRAZORPAY_KEY_ID=...RAZORPAY_KEY_SECRET=...https://yourdomain.com)./api/payments/config and enables/disables donation and subscription Pay Now actions.key_id and key_secret.GET /api/churches/payment-configPOST /api/churches/payment-configchurch_id./api/payments/config, order, and verify routes now resolve keys from the logged-in user’s church..env are now fallback only (mainly for backward compatibility before migration).This project now supports direct super-admin access by email.
.env under SUPER_ADMIN_EMAILS.POST /api/auth/sync-profile once after login, or run db/step4_step5_bootstrap.sql to seed churches + admin row manually.POST /api/auth/sync-profile { "full_name": "Sonu", "church_id": "your-church-uuid" }
POST /api/auth/update-profile { "full_name": "Sonu", "avatar_url": "https://...", "address": "Kochi", "subscription_amount": 500 }
GET /api/admins/list
POST /api/admins/pre-register-member { "email": "member@church.com", "full_name": "Member Name", "membership_id": "M-1003", "address": "Kochi", "subscription_amount": 500, "church_id": "optional-uuid-for-super-admin" }
POST /api/admins/grant { "email": "target@church.com", "church_id": "optional-uuid" }
POST /api/admins/revoke { "email": "target@church.com" }
Rules:
public.users (ask user to login once first).public.users.403 (This email is not registered) and frontend signs the user out.admin users see admin tools; member users see a member dashboard (profile, church, subscriptions, receipts, donation summary, and history).auth.users.id into public.users.auth_user_id by matching email.db/manual_members_seed.sql to seed member users and dashboard data manually.public.users.id is now auto-generated and public.users.auth_user_id is filled automatically on first login./signin, /dashboard, /profile, /admin-tools, and /signout.Pay Now button is active only when dues are pending; otherwise it is shown as inactive..../delete-impact) before force delete to reduce accidental data loss.public.subscription_events.active -> overdue).subscription_events by member id and refreshes the dashboard automatically.POST /api/subscriptions/reconcile-overdue
POST /api/subscriptions/reconcile-overdue?scope=all # super-admin only