Skip to content

Commit 975a1f7

Browse files
authored
Merge pull request #27 from adab-tech/claude/globalopportunities-deploy-yukhn1
Bring docs in line with the actual deploy, drop dead Fly.io path
2 parents 707ec60 + 8636717 commit 975a1f7

9 files changed

Lines changed: 71 additions & 259 deletions

File tree

.claude/skills/opportunityfinder-dev/SKILL.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,10 @@ Dev deps live in `backend/requirements-dev.txt` (pytest, ruff). Prod deps in
410410

411411
## Deploy
412412

413-
- **Render** (no CLI): docs/DEPLOY-RENDER.md
414-
- **Fly.io**: `scripts/deploy-fly.ps1` (app `adab-opportunityfinder`, region lhr);
415-
needs `flyctl auth login` or `FLY_API_TOKEN`
413+
- **Backend (Render)**, no CLI: docs/DEPLOY-RENDER.md
414+
- **Frontend (Cloudflare Workers)**, edge-served static assets + API proxy:
415+
docs/DEPLOY-CLOUDFLARE-WORKERS.md — this is what actually serves
416+
`globalopportunities.app` today
416417
- **Docker local**: `docker compose up --build` (Postgres + API)
417418
- `/health` returns 503 when the DB is down — platform health checks rely on this.
418419
- Container runs as non-root `appuser`; keep it that way.

.github/SECRETS.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@ Configure under **Settings → Secrets and variables → Actions** for [adab-tec
44

55
| Secret | Required | Purpose |
66
|--------|----------|---------|
7-
| `FLY_API_TOKEN` | For GitHub deploy | Deploy to Fly.io via Actions (`workflow_dispatch`) |
87
| `GOOGLE_API_KEY` | Optional | Google Custom Search API (higher quota than scrape fallback) |
98
| `GOOGLE_CSE_ID` | Optional | Custom Search Engine ID (pair with `GOOGLE_API_KEY`) |
109

11-
### Fly.io deploy token
12-
13-
After `flyctl auth login` on your machine:
14-
15-
```powershell
16-
flyctl tokens create deploy -a adab-opportunityfinder
17-
gh secret set FLY_API_TOKEN --repo adab-tech/OpportunityFinder
18-
```
19-
20-
Then run **Actions → Deploy to Fly.io → Run workflow** on GitHub, or use `.\scripts\deploy-fly.ps1` locally.
21-
2210
CI runs without these secrets (SQLite + smoke tests only). For production scraping performance, add both keys from [Google Programmable Search](https://developers.google.com/custom-search/v1/introduction).
2311

12+
Production deploy credentials (Render, Cloudflare) live in each platform's
13+
own dashboard, not as GitHub Actions secrets — see docs/DEPLOY-RENDER.md and
14+
docs/DEPLOY-CLOUDFLARE-WORKERS.md.
15+
2416
**Local setup:** copy `backend/.env.example` to `backend/.env` and fill values.
2517

2618
**Sync from local `.env` (run on your machine):**

.github/workflows/deploy-fly.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.

README.md

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Global Opportunities
22

3-
Global Opportunities is an AI-assisted opportunity discovery app for scholarships, fellowships, grants, and jobs.
3+
Global Opportunities is an AI-assisted discovery site for scholarships, fellowships, grants, and jobs — plain-English summaries, real deadlines, refreshed automatically.
44

5-
## What is included
5+
**Live:** https://globalopportunities.app
66

7-
- `backend/` - FastAPI API, database, scraping, and scheduled refresh jobs
8-
- `frontend/` - static UI that talks to the API
9-
- `opportunities.db` - local SQLite database used during development
7+
## Architecture
108

11-
## Quick start (Windows)
9+
- `backend/` — FastAPI API, Postgres (via [Neon](https://neon.tech)), scraping/RSS ingest, and scheduled refresh jobs. Deployed on [Render](https://render.com).
10+
- `frontend/` — static UI, no build step, served from Cloudflare's edge via a Worker (`worker/index.js` + `wrangler.jsonc`). The same worker proxies `/api/*`, `/health`, `/docs`, `/openapi.json`, and `/redoc` to the Render backend, so the browser only ever sees one origin.
11+
- `admin.html` — analytics + a moderation queue, gated behind email/password admin login (`app/routes/admin_auth.py`).
1212

13-
Double-click `start.bat` in the repo root. The app opens at http://127.0.0.1:8000/ with curated opportunities loaded immediately; a background scrape adds more within a few minutes. Click **Find New** anytime to refresh.
13+
See [docs/DEPLOY-RENDER.md](docs/DEPLOY-RENDER.md) and [docs/DEPLOY-CLOUDFLARE-WORKERS.md](docs/DEPLOY-CLOUDFLARE-WORKERS.md) for the full deploy story, and [docs/DEPLOY.md](docs/DEPLOY.md) for local development and Docker.
1414

1515
## Local development
1616

@@ -25,48 +25,29 @@ python -m pip install -r requirements.txt
2525
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
2626
```
2727

28-
### Frontend
29-
30-
Serve the `frontend/` folder with any static web server, then point it at the API.
31-
32-
If the frontend and backend are deployed together on the same domain, the app will use `/api/v1` automatically.
33-
If they are deployed separately, set the API base before loading `js/app.js`:
34-
35-
```html
36-
<script>
37-
window.OPPORTUNITYFINDER_API_BASE = 'https://your-api-host.example.com/api/v1';
38-
</script>
39-
```
28+
Copy `backend/.env.example` to `backend/.env` first. With nothing else set, the app runs on SQLite and logs alert/save-confirmation emails to the console instead of sending them.
4029

41-
The repo also includes `frontend/config.js` as the deployment hook for that value.
42-
Edit it when you want to point the static frontend at a separate backend host.
43-
44-
If you are running locally from `file://`, the app falls back to `http://127.0.0.1:8000/api/v1`.
30+
### Frontend
4531

46-
## Optional search API keys
32+
Serve `frontend/` with any static web server; it talks to the API at the same origin (`/api/v1`) by default. To point it at a separately-hosted backend, set `window.OPPORTUNITYFINDER_API_BASE` in `frontend/config.js`.
4733

48-
`backend/.env.example` includes optional Google Custom Search settings:
34+
### Windows quick start
4935

50-
- `GOOGLE_API_KEY`
51-
- `GOOGLE_CSE_ID`
36+
Double-click `start.bat` — creates a venv, installs backend dependencies, and opens the app at http://127.0.0.1:8000/. Local only, not how production runs.
5237

53-
Without those keys, the scraper falls back to public search scraping.
38+
## Optional discovery API keys
5439

55-
## Deploy to the web
40+
`backend/.env.example` documents `GOOGLE_API_KEY` / `GOOGLE_CSE_ID` (Google Custom Search) and `YOU_API_KEY` (You.com) — both optional. Without them, the scraper falls back to public search scraping.
5641

57-
**Recommended:** one container serves the UI and API on the same domain (simplest for users).
42+
## Email alerts
5843

59-
| Method | Guide | CLI needed? |
60-
|--------|--------|-------------|
61-
| **Render** (easiest public URL) | [docs/DEPLOY-RENDER.md](docs/DEPLOY-RENDER.md) | No — GitHub + dashboard |
62-
| Fly.io | [docs/DEPLOY.md](docs/DEPLOY.md) | Yes (`flyctl auth login`) |
63-
| Docker + Postgres locally | `docker compose up --build`http://localhost:8000 | Docker Desktop |
44+
Alert and save-confirmation emails are logged to the console by default — no provider required to run or test the feature end-to-end. Set `RESEND_API_KEY`, `BREVO_API_KEY`, or `SENDGRID_API_KEY` to send them for real; see `backend/app/services/email_sender.py`.
6445

65-
Production uses **PostgreSQL** (`DATABASE_URL`) and **RSS feeds** for reliable ingest (ReliefWeb, Scholars4Dev, Opportunity Desk, and more). Optional Google CSE keys still improve discovery.
46+
## Tests
6647

67-
## Production notes
48+
```powershell
49+
cd backend && python -m pytest tests -v
50+
cd frontend && npm test
51+
```
6852

69-
- Set `DATABASE_URL` to Postgres in production (see `docker-compose.yml` or Fly Postgres).
70-
- `ENABLE_SCHEDULER=true` on a single instance runs periodic RSS + scrape jobs.
71-
- `CORS_ORIGINS` defaults to `*`; restrict when the frontend is on another host.
72-
- Health: `GET /health` (used by Fly.io checks).
53+
CI (`.github/workflows/ci.yml`) runs both suites plus `ruff` lint on every push/PR to `main`.

docs/DEPLOY-CLOUDFLARE-WORKERS.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
# Move the frontend to Cloudflare Workers (edge-served static assets)
1+
# Cloudflare Workers (edge-served static assets)
22

3-
Today, `globalopportunities.app` is a Cloudflare-proxied DNS record pointing
4-
at the Render service, which serves both the API and the static frontend
5-
(`frontend/`) from one Docker container. Every visitor's HTML/CSS/JS still
6-
makes a round trip to Render's origin server (and can hit a cold start on
7-
Render's free tier if it's been idle).
3+
**This is live in production.** `globalopportunities.app` is a Cloudflare
4+
Worker (`worker/index.js` + `wrangler.jsonc`) that serves the static frontend
5+
(`frontend/`) from the edge and proxies API calls to the Render backend
6+
server-side. This doc is kept as the reference for how that's set up and for
7+
reproducing it (e.g. a fresh Cloudflare account, or a new environment).
88

9-
This moves the static frontend onto Cloudflare's edge network instead —
10-
faster worldwide, and immune to Render cold-starts for anything that isn't
11-
an API call — **without touching the backend or admin auth at all.**
9+
Before this move, `globalopportunities.app` was a Cloudflare-proxied DNS
10+
record pointing directly at the Render service, which served both the API
11+
and the static frontend from one Docker container — every visitor's
12+
HTML/CSS/JS made a round trip to Render's origin server, with a possible
13+
cold start on Render's free tier if it had been idle. Moving the static
14+
frontend onto Cloudflare's edge network instead is faster worldwide, and
15+
immune to Render cold-starts for anything that isn't an API call — **without
16+
touching the backend or admin auth at all.**
1217

1318
> **Why "Workers" and not "Pages"?** Cloudflare's own current guidance:
1419
> "If you are starting a new project, use Workers instead of Pages. Pages

docs/DEPLOY-RENDER.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ No local CLI required — deploy from the Render dashboard in ~5 minutes.
3333

3434
## Custom domain — globalopportunities.app
3535

36+
**This is no longer how the live domain is wired up.** `globalopportunities.app`
37+
now points at a Cloudflare Worker that serves the frontend from the edge and
38+
proxies API calls to this Render service — see
39+
[DEPLOY-CLOUDFLARE-WORKERS.md](DEPLOY-CLOUDFLARE-WORKERS.md) for the actual
40+
current setup. The steps below are kept for reference if you ever want to run
41+
this backend standalone, pointed at directly by DNS instead of through the
42+
Worker.
43+
3644
In the Render web service → **Settings → Custom Domains**, add both:
3745

3846
- `globalopportunities.app` (apex)
@@ -60,8 +68,9 @@ so the app's own links point at the real domain instead of localhost:
6068
|----------|-------|
6169
| `PUBLIC_BASE_URL` | `https://globalopportunities.app` |
6270

63-
(`CORS_ORIGINS` can stay `*` — the frontend is served same-origin from this
64-
same service, so it isn't relying on cross-origin CORS.)
71+
(`CORS_ORIGINS` can stay `*` — the frontend is served from Cloudflare's edge
72+
via a Worker that proxies API calls back to this service server-side, not by
73+
the browser making cross-origin requests; see docs/DEPLOY-CLOUDFLARE-WORKERS.md.)
6574

6675
## Free tier notes
6776

docs/DEPLOY.md

Lines changed: 16 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,37 @@
1-
# Deploy OpportunityFinder
1+
# Deploy Global Opportunities
22

3-
One domain, one container: FastAPI serves `/api/v1` and the static `frontend/` on the same origin (no CORS headaches).
3+
Production is two pieces: the **backend** (FastAPI + Postgres) on Render, and the **frontend** (static, no build step) on Cloudflare Workers at the edge, which proxies API calls back to Render so the browser only ever sees one origin. See:
44

5-
## Option A — Docker Compose (local production test)
5+
- [DEPLOY-RENDER.md](DEPLOY-RENDER.md) — backend + database, from zero to a live URL
6+
- [DEPLOY-CLOUDFLARE-WORKERS.md](DEPLOY-CLOUDFLARE-WORKERS.md) — moving the frontend to the edge and wiring up the custom domain
7+
8+
This doc covers local development only.
9+
10+
## Docker Compose (local, with Postgres)
611

712
Requirements: [Docker Desktop](https://www.docker.com/products/docker-desktop/)
813

9-
```powershell
10-
cd "C:\Users\Adamu\Desktop\Project 101"
14+
```
1115
docker compose up --build
1216
```
1317

1418
Open **http://localhost:8000/** — Postgres runs in the `db` service; data persists in the `pgdata` volume.
1519

1620
Stop: `docker compose down` (add `-v` to wipe the database).
1721

18-
## Option B — Fly.io (public URL)
19-
20-
1. Install the [Fly CLI](https://fly.io/docs/hands-on/install-flyctl/) and run `fly auth login`.
21-
2. From the repo root:
22-
23-
**One-command deploy** (after `fly auth login`):
24-
25-
```powershell
26-
cd "C:\Users\Adamu\Desktop\Project 101"
27-
.\scripts\deploy-fly.ps1
28-
```
29-
30-
App name: **`adab-opportunityfinder`****https://adab-opportunityfinder.fly.dev**
31-
32-
Manual steps (equivalent):
33-
34-
```powershell
35-
fly launch --no-deploy --name adab-opportunityfinder --region lhr
36-
fly postgres create --name adab-opportunityfinder-db --region lhr --vm-size shared-cpu-1x --volume-size 1
37-
fly postgres attach adab-opportunityfinder-db -a adab-opportunityfinder
38-
fly deploy -a adab-opportunityfinder
39-
fly open -a adab-opportunityfinder
40-
```
41-
42-
`DATABASE_URL` from Fly Postgres is normalized automatically for SQLAlchemy + psycopg2.
43-
44-
### Custom domain on Fly
45-
46-
```powershell
47-
fly certs add finder.yourdomain.com
48-
```
49-
50-
Point DNS (CNAME) to the hostname Fly prints. HTTPS is automatic.
51-
52-
## Option C — Railway / Render
53-
54-
1. Create a **PostgreSQL** database on the platform.
55-
2. Deploy from this GitHub repo (`adab-tech/OpportunityFinder`) with:
56-
- **Build:** Dockerfile at repo root
57-
- **Start:** handled by Dockerfile (`PORT` is set by the platform)
58-
3. Environment variables:
59-
60-
| Variable | Value |
61-
|----------|--------|
62-
| `DATABASE_URL` | Postgres URL from the provider (use `postgresql+psycopg2://...` if needed) |
63-
| `ENABLE_SCHEDULER` | `true` (only on **one** instance if you scale horizontally) |
64-
| `CORS_ORIGINS` | `*` or your static site origin if split later |
65-
66-
## Split frontend later (optional)
67-
68-
By default the UI uses same-origin `/api/v1`. To host the UI on GitHub Pages and API elsewhere, set in `frontend/config.js` before deploy:
69-
70-
```javascript
71-
window.OPPORTUNITYFINDER_API_BASE = 'https://your-api.fly.dev/api/v1';
72-
```
73-
74-
And set `CORS_ORIGINS` on the API to your Pages URL.
75-
7622
## Environment reference
7723

78-
Copy `backend/.env.example` to `backend/.env` for local dev.
24+
Copy `backend/.env.example` to `backend/.env` for local dev. Full definitions live in `backend/app/config.py`; highlights:
7925

8026
| Variable | Purpose |
8127
|----------|---------|
8228
| `DATABASE_URL` | SQLite locally; Postgres in production. Prefer [Neon](https://neon.tech)'s free tier over a platform's own free Postgres (e.g. Render) — Render's free Postgres auto-deletes the database 30 days after creation, which has already caused a production outage; Neon's free tier only autosuspends compute when idle and never deletes data. |
83-
| `ENABLE_SCHEDULER` | Background RSS + scrape jobs |
84-
| `CORS_ORIGINS` | Comma-separated origins, or `*` |
85-
| `PORT` | Set by Fly/Railway/Render (uvicorn listens here) |
86-
| `GOOGLE_API_KEY` / `GOOGLE_CSE_ID` | Optional; improves discovery |
29+
| `ENABLE_SCHEDULER` | Background RSS + scrape jobs — set `true` on exactly one instance if you scale horizontally. |
30+
| `CORS_ORIGINS` | Comma-separated origins, or `*`. |
31+
| `GOOGLE_API_KEY` / `GOOGLE_CSE_ID` / `YOU_API_KEY` | Optional; improve discovery beyond the scraping fallback. |
8732
| `RESEND_API_KEY` / `BREVO_API_KEY` / `SENDGRID_API_KEY` | Optional; unset means alert/save-confirmation emails are logged, not sent. If more than one is set, Resend takes priority, then Brevo. |
33+
| `ADMIN_EMAIL` / `ADMIN_PASSWORD_HASH` / `SESSION_SECRET_KEY` | Required together for admin login (analytics + moderation queue). Generate the hash with `backend/scripts/hash_admin_password.py`. Unset means admin endpoints refuse every request. |
34+
| `PUBLIC_BASE_URL` | Builds the manage-your-alerts link in outgoing emails; set to your real domain in production. |
8835

8936
## Health check
9037

@@ -95,12 +42,6 @@ Copy `backend/.env.example` to `backend/.env` for local dev.
9542

9643
1. DB tables created if missing.
9744
2. Curated seeds if the database is empty.
98-
3. RSS ingest from stable feeds (ReliefWeb, Scholars4Dev, Opportunity Desk, etc.).
45+
3. RSS ingest from stable feeds (ReliefWeb, Scholars4Dev, Opportunity Desk, and more).
9946
4. Background scrape if fewer than ~25 active listings.
10047
5. Scheduler (every 6h by default) if `ENABLE_SCHEDULER=true`.
101-
102-
## Next steps (product)
103-
104-
- Domain registered: `globalopportunities.app` — see docs/DEPLOY-RENDER.md for the custom-domain DNS steps.
105-
- Add email alerts (Resend + saved searches).
106-
- Add Meilisearch for faster full-text search at scale.

fly.toml

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)