# SJSMP Deployment Guide

## Infrastructure Stack

| Layer | Service | Cost | Purpose |
|-------|---------|------|---------|
| Hosting | Cloudflare Pages | Free | Static site (HTML/CSS/JS) |
| Backend | Cloudflare Workers | Free tier (100k req/day) | Email/SMS relay, bulletin scraper, webhooks |
| Storage | Cloudflare D1 (SQLite) | Free tier (5GB) | Replaces localStorage for production |
| File storage | Cloudflare R2 | ~$0.015/GB/mo | Member photos (private) |
| Email | Resend | Free (3k/mo) | Transactional email |
| SMS | Twilio | $1/mo + $0.0079/msg | SMS send/receive |
| Domain | sjsmusic.org | ~$12/yr | DNS, email routing |

**Estimated monthly cost at launch:** ~$1–3/month

---

## Step 1 — Domain (sjsmusic.org)

1. Purchase `sjsmusic.org` at Cloudflare Registrar (same account = easiest DNS management)
2. Add DNS records (Cloudflare Pages will provide these automatically on deploy)
3. Set up email routing: Cloudflare Email Routing (free) → forward `replies@sjsmusic.org` to your personal Gmail

---

## Step 2 — Cloudflare Setup

```bash
# Install Wrangler CLI
npm install -g wrangler

# Login
wrangler login

# Create Pages project
wrangler pages project create sjsmp

# Deploy site
wrangler pages deploy . --project-name sjsmp
```

---

## Step 3 — Resend (Email)

1. Create account at resend.com
2. Add domain: verify `sjsmusic.org` by adding DNS TXT records (Cloudflare makes this easy)
3. Get API key from dashboard
4. Create Worker environment variable: `RESEND_API_KEY = re_xxxxxxxxxxxx`

**Reply routing:** In Resend dashboard, add a webhook for inbound emails to `replies@sjsmusic.org`. Set webhook URL to `https://sjsmusic.workers.dev/api/email/reply-webhook`.

---

## Step 4 — Twilio (SMS)

1. Create account at twilio.com
2. Purchase phone number (+1 area code of your choice): ~$1/month
3. Get Account SID and Auth Token
4. Add Worker environment variables:
   ```
   TWILIO_ACCOUNT_SID = ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
   TWILIO_AUTH_TOKEN  = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
   TWILIO_FROM_NUMBER = +1XXXXXXXXXX
   ```
5. Set Twilio webhook for inbound SMS: `https://sjsmusic.workers.dev/api/sms/inbound`

---

## Step 5 — Cloudflare Workers Backend

Create `worker/index.js`:

```javascript
// Required environment variables (set in Cloudflare dashboard):
// RESEND_API_KEY, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;

    // CORS for dashboard
    const headers = {
      'Access-Control-Allow-Origin': 'https://sjsmusic.org',
      'Content-Type': 'application/json'
    };

    if (request.method === 'OPTIONS') return new Response(null, { headers });

    // ── Send email via Resend ──────────────────────────────
    if (path === '/api/email/send' && request.method === 'POST') {
      const body = await request.json();
      const res = await fetch('https://api.resend.com/emails', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          from: 'St. Joseph Shrine Music Program <no-reply@sjsmusic.org>',
          to: body.to,
          subject: body.subject,
          html: body.html,
          reply_to: body.replyTo || 'replies@sjsmusic.org'
        })
      });
      const data = await res.json();
      return new Response(JSON.stringify({ ok: res.ok, data }), { headers });
    }

    // ── Send SMS via Twilio ────────────────────────────────
    if (path === '/api/sms/send' && request.method === 'POST') {
      const body = await request.json();
      const recipients = Array.isArray(body.to) ? body.to : [body.to];
      const results = [];
      for (const to of recipients) {
        const params = new URLSearchParams({
          To: to, From: env.TWILIO_FROM_NUMBER, Body: body.body
        });
        const res = await fetch(
          `https://api.twilio.com/2010-04-01/Accounts/${env.TWILIO_ACCOUNT_SID}/Messages.json`,
          { method: 'POST', headers: {
              'Authorization': 'Basic ' + btoa(`${env.TWILIO_ACCOUNT_SID}:${env.TWILIO_AUTH_TOKEN}`),
              'Content-Type': 'application/x-www-form-urlencoded'
            }, body: params }
        );
        results.push(await res.json());
      }
      return new Response(JSON.stringify({ ok: true, results }), { headers });
    }

    // ── Inbound SMS (Twilio webhook) ───────────────────────
    if (path === '/api/sms/inbound' && request.method === 'POST') {
      const form = await request.formData();
      const from = form.get('From');
      const body = form.get('Body');
      // Parse attendance message
      // Store in D1: INSERT INTO sms_inbox (from, body, received_at) VALUES (?, ?, ?)
      // Look up member by phone number, parse attendance command
      // ... (full implementation in worker/sms-handler.js)
      return new Response('<?xml version="1.0"?><Response></Response>',
        { headers: { 'Content-Type': 'text/xml' } });
    }

    // ── Bulletin sync (cron trigger) ───────────────────────
    if (path === '/api/bulletin/sync' && request.method === 'POST') {
      const url = BulletinParser.latestBulletinURL();
      const res = await fetch(url);
      if (!res.ok) return new Response(JSON.stringify({ ok: false, error: 'Bulletin not found' }), { headers });
      const pdfBuffer = await res.arrayBuffer();
      // Extract text from PDF (use pdf-parse or pdfjs-dist in Worker)
      // const text = await extractPdfText(pdfBuffer);
      // const bulletinDate = url.match(/(\d{4}-\d{2}-\d{2})\.pdf/)[1];
      // const entries = BulletinParser.parseText(text, bulletinDate);
      // return new Response(JSON.stringify({ ok: true, entries }), { headers });
      return new Response(JSON.stringify({ ok: true, entries: [], note: 'PDF parsing library required' }), { headers });
    }

    // ── Email reply webhook (Resend) ───────────────────────
    if (path === '/api/email/reply-webhook' && request.method === 'POST') {
      const payload = await request.json();
      // Store reply in D1 for dashboard display
      // Optionally forward to director's personal email
      return new Response('ok', { status: 200 });
    }

    return new Response('Not found', { status: 404 });
  },

  // Cron job: runs every Sunday at midnight to sync bulletin
  async scheduled(event, env, ctx) {
    // ctx.waitUntil(syncBulletin(env));
  }
};
```

**wrangler.toml:**
```toml
name = "sjsmp"
main = "worker/index.js"
compatibility_date = "2024-01-01"

[triggers]
crons = ["0 0 * * 0"]   # Every Sunday midnight (bulletin sync)

[[d1_databases]]
binding = "DB"
database_name = "sjsmp"
database_id = ""   # Fill in after: wrangler d1 create sjsmp
```

---

## Step 6 — Connect dashboard to backend

In `index.html` and `pages/admin.html`, add before the closing `</body>`:

```html
<script>
  window._API_BASE = 'https://sjsmusic.workers.dev';
</script>
```

Once this is set, `Messaging.hasBackend()` returns true and all email/SMS calls go live.

---

## Step 7 — Photo uploads (R2)

The admin dashboard has a "Upload Photo" button on each member card. In production:
1. Admin requests a pre-signed upload URL: `POST /api/photos/upload-url`
2. Worker generates a temporary R2 upload URL
3. Browser uploads directly to R2
4. Worker stores the R2 key in the member's D1 record
5. Photos are served via: `GET /api/photos/:memberId` — Worker checks auth before serving

---

## Follow-up items (post-launch)

- [ ] Mobile app (Claude Code) — push notifications, RSVP, calendar integration
- [ ] SMS attendance line — full inbound parsing + acknowledgement (worker/sms-handler.js)
- [ ] DSMF 501(c)(3) incorporation
- [ ] Costs tracker on Foundation page (Twilio, R2, D1, domain = ~$15/yr)
- [ ] Replace remaining localStorage with D1 for multi-device sync
- [ ] Photographer upload portal (time-limited pre-signed R2 URLs)
- [ ] Automated wedding follow-up email (Resend + cron)
- [ ] Bulletin PDF text extraction (pdf-parse in Worker)
- [ ] Repertoire import from spreadsheet
