Claude Claude Opus 4.8 commited on
Commit
abf3c4b
Β·
unverified Β·
1 Parent(s): 24361e0

Switch deploy to HF Static Space (frontend) + separate API

Browse files

Hugging Face only offers free Static Spaces (Docker requires a paid plan),
so the frontend now deploys to a Static Space and the API to any Python
host:

- README front matter -> sdk: static with app_build_command building
frontend/ and serving frontend/dist.
- Frontend resolves the API base at runtime from the HF Space variable
DOCUASK_API_URL (via window.huggingface.variables), then VITE_API_URL,
then /api β€” so the API URL changes without a rebuild.
- Backend CORS default now includes the Static Space origin; backend
Dockerfile honors and a Procfile is added for native-Python hosts.
- Deploy docs rewritten for the split; the single-image Docker option
(root Dockerfile + app/server.py) is kept as an alternative.

Sync workflow and tests unchanged in behavior; 21 backend tests pass and
the frontend builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WN4QRr6dTE2W7hQ2SDnLmY

.github/workflows/sync-to-hf.yml CHANGED
@@ -1,8 +1,8 @@
1
  name: Sync to Hugging Face Space
2
 
3
- # Mirrors the default branch to a Hugging Face Space on every push, so the live
4
- # demo tracks main. The Space builds the root Dockerfile (see README front
5
- # matter).
6
  #
7
  # One-time setup (GitHub β†’ Settings β†’ Secrets and variables β†’ Actions):
8
  # - Secret HF_TOKEN : a Hugging Face access token with write scope
 
1
  name: Sync to Hugging Face Space
2
 
3
+ # Mirrors the default branch to a Hugging Face Static Space on every push, so
4
+ # the live demo tracks main. The Space builds the frontend (see the
5
+ # app_build_command in the README front matter) and serves frontend/dist.
6
  #
7
  # One-time setup (GitHub β†’ Settings β†’ Secrets and variables β†’ Actions):
8
  # - Secret HF_TOKEN : a Hugging Face access token with write scope
PROJECT_SPEC.md CHANGED
@@ -119,9 +119,10 @@ Goal: live URL + green CI + a README a recruiter can skim.
119
  - README: one-paragraph description, screenshot/GIF of the chat + dashboard,
120
  live demo link, run instructions, tech stack.
121
 
122
- > Note: this project deploys as a single-image **Hugging Face Space** (Docker),
123
- > which serves the frontend and API from one origin β€” replacing the split
124
- > Render/Vercel plan above.
 
125
 
126
  **Acceptance criteria**
127
 
 
119
  - README: one-paragraph description, screenshot/GIF of the chat + dashboard,
120
  live demo link, run instructions, tech stack.
121
 
122
+ > Note: this project deploys the frontend to a **Hugging Face Static Space**
123
+ > (HF builds `frontend/` and serves it) and the API to any Python host, wired by
124
+ > the `DOCUASK_API_URL` Space variable. A single-image Docker option
125
+ > (`Dockerfile` + `app/server.py`) is also included for one-origin hosting.
126
 
127
  **Acceptance criteria**
128
 
README.md CHANGED
@@ -3,8 +3,9 @@ title: DocuAsk
3
  emoji: πŸ“„
4
  colorFrom: green
5
  colorTo: gray
6
- sdk: docker
7
- app_port: 7860
 
8
  pinned: false
9
  license: mit
10
  ---
@@ -94,25 +95,45 @@ every push.
94
  | POST | `/feedback` | Attach πŸ‘/πŸ‘Ž (`up`/`down`) to an interaction. |
95
  | GET | `/stats` | Totals, median latency, thumbs-up rate, over-time. |
96
 
97
- ## Deploy (Hugging Face Spaces)
98
 
99
- The root [`Dockerfile`](./Dockerfile) builds a **single image** that serves the
100
- React frontend and the API from one origin (the API is mounted under `/api`), so
101
- you get one live URL with no CORS to configure.
102
 
103
- 1. Create a new **Space** β†’ **Docker** SDK (blank template).
104
- 2. Push this repository to the Space (or connect the GitHub repo). The Space
105
- reads the YAML front matter at the top of this README (`sdk: docker`,
106
- `app_port: 7860`) and builds the root `Dockerfile`.
107
- 3. Wait for the build; the app comes up at your Space URL. Paste that URL into
108
- the **Live demo** link above.
109
 
110
- Telemetry uses `/tmp/docuask.db` by default (resets on restart). To persist it,
111
- add HF **persistent storage** and set `DOCUASK_DB=/data/docuask.db` in the
112
- Space's variables.
113
 
114
- **Keep the demo in sync automatically.** The
115
- [`sync-to-hf`](./.github/workflows/sync-to-hf.yml) workflow mirrors `main` to
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  your Space on every push. Configure it once under **Settings β†’ Secrets and
117
  variables β†’ Actions**:
118
 
@@ -122,9 +143,10 @@ variables β†’ Actions**:
122
 
123
  Until `HF_TOKEN` is set the workflow no-ops, so it never fails the branch.
124
 
125
- > Prefer split hosting (e.g. static frontend + separate API)? Set
126
- > `VITE_API_URL` at frontend build time to the API origin and add that origin to
127
- > the backend's `CORS_ORIGINS`. See the `.env.example` files.
 
128
 
129
  ## Project structure
130
 
@@ -156,7 +178,7 @@ docuask/
156
  - **Persistence:** SQLite (interaction telemetry)
157
  - **Tests:** pytest
158
  - **CI:** GitHub Actions (pytest + build on every push)
159
- - **Deploy:** Docker (single-image Hugging Face Space); Docker Compose locally
160
 
161
  ## Roadmap
162
 
 
3
  emoji: πŸ“„
4
  colorFrom: green
5
  colorTo: gray
6
+ sdk: static
7
+ app_build_command: cd frontend && npm ci && npm run build
8
+ app_file: frontend/dist/index.html
9
  pinned: false
10
  license: mit
11
  ---
 
95
  | POST | `/feedback` | Attach πŸ‘/πŸ‘Ž (`up`/`down`) to an interaction. |
96
  | GET | `/stats` | Totals, median latency, thumbs-up rate, over-time. |
97
 
98
+ ## Deploy
99
 
100
+ The frontend deploys to a **Hugging Face Static Space** and the API to any host
101
+ that runs a Python web process. They're wired together with one Space variable β€”
102
+ no rebuild needed to change the API URL.
103
 
104
+ ### 1. Frontend β†’ Hugging Face Static Space
 
 
 
 
 
105
 
106
+ The README front matter (`sdk: static`) tells HF to run the
107
+ `app_build_command` (`cd frontend && npm ci && npm run build`) and serve
108
+ `frontend/dist`.
109
 
110
+ 1. Create a new **Space** β†’ **Static** SDK, owner `Sri-28`, name `docuask`.
111
+ 2. Push this repo to the Space (the [`sync-to-hf`](./.github/workflows/sync-to-hf.yml)
112
+ workflow does this automatically β€” see below).
113
+ 3. In the Space's **Settings β†’ Variables**, add `DOCUASK_API_URL` set to your
114
+ deployed API's URL. The frontend reads it at runtime via
115
+ `window.huggingface.variables`, so you can change it without rebuilding.
116
+
117
+ The Space serves at **https://sri-28-docuask.static.hf.space**.
118
+
119
+ ### 2. Backend β†’ your host
120
+
121
+ The API is a standard uvicorn app; deploy it however you like:
122
+
123
+ - **Docker host** (Fly.io, Railway, …): build [`backend/Dockerfile`](./backend/Dockerfile)
124
+ β€” it honors `$PORT`.
125
+ - **Native Python host** (Render web service, Railway, …): use
126
+ [`backend/Procfile`](./backend/Procfile) β€”
127
+ `uvicorn app.main:app --host 0.0.0.0 --port $PORT`.
128
+
129
+ The Static Space origin (`https://sri-28-docuask.static.hf.space`) is already in
130
+ the backend's default CORS allow-list; add more origins via the `CORS_ORIGINS`
131
+ env var. Set `DOCUASK_DB` to a path on persistent storage to keep telemetry
132
+ across restarts. See [`backend/.env.example`](./backend/.env.example).
133
+
134
+ ### Keep the demo in sync automatically
135
+
136
+ The [`sync-to-hf`](./.github/workflows/sync-to-hf.yml) workflow mirrors `main` to
137
  your Space on every push. Configure it once under **Settings β†’ Secrets and
138
  variables β†’ Actions**:
139
 
 
143
 
144
  Until `HF_TOKEN` is set the workflow no-ops, so it never fails the branch.
145
 
146
+ > **One-origin alternative:** the root [`Dockerfile`](./Dockerfile) +
147
+ > [`app/server.py`](./backend/app/server.py) build a single image that serves the
148
+ > frontend and API together under one origin (API mounted at `/api`, no CORS).
149
+ > Use this on any Docker host if you'd rather run one service than two.
150
 
151
  ## Project structure
152
 
 
178
  - **Persistence:** SQLite (interaction telemetry)
179
  - **Tests:** pytest
180
  - **CI:** GitHub Actions (pytest + build on every push)
181
+ - **Deploy:** Hugging Face Static Space (frontend) + any Python host (API); Docker Compose locally
182
 
183
  ## Roadmap
184
 
backend/Dockerfile CHANGED
@@ -16,4 +16,6 @@ RUN apt-get update \
16
  && apt-get install -y --no-install-recommends curl libgomp1 \
17
  && rm -rf /var/lib/apt/lists/*
18
 
19
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
 
 
 
16
  && apt-get install -y --no-install-recommends curl libgomp1 \
17
  && rm -rf /var/lib/apt/lists/*
18
 
19
+ # Honor $PORT when the host injects one (Railway, Fly, Render, …); default 8000
20
+ # for docker-compose, which maps and health-checks 8000.
21
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
backend/Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: uvicorn app.main:app --host 0.0.0.0 --port $PORT
backend/app/main.py CHANGED
@@ -26,9 +26,13 @@ logger = logging.getLogger("docuask")
26
  # exhaust memory. 10 MB comfortably covers real documents.
27
  MAX_UPLOAD_BYTES = 10 * 1024 * 1024
28
 
29
- # Comma-separated list of allowed origins. Defaults cover the Vite dev server
30
- # and a locally served production build. Override in deployment via env var.
31
- _DEFAULT_ORIGINS = "http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173"
 
 
 
 
32
  ALLOWED_ORIGINS = [
33
  origin.strip()
34
  for origin in os.getenv("CORS_ORIGINS", _DEFAULT_ORIGINS).split(",")
 
26
  # exhaust memory. 10 MB comfortably covers real documents.
27
  MAX_UPLOAD_BYTES = 10 * 1024 * 1024
28
 
29
+ # Comma-separated list of allowed origins. Defaults cover the Vite dev server,
30
+ # a locally served production build, and the DocuAsk Hugging Face Static Space
31
+ # (the deployed frontend). Override in deployment via the CORS_ORIGINS env var.
32
+ _DEFAULT_ORIGINS = (
33
+ "http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,"
34
+ "https://sri-28-docuask.static.hf.space"
35
+ )
36
  ALLOWED_ORIGINS = [
37
  origin.strip()
38
  for origin in os.getenv("CORS_ORIGINS", _DEFAULT_ORIGINS).split(",")
frontend/.env.example CHANGED
@@ -1,7 +1,11 @@
1
  # Frontend build-time configuration.
2
 
3
- # Base URL for the API. Leave UNSET for:
4
- # - local dev (Vite proxies /api to the backend), and
5
- # - the single-image Hugging Face deploy (API is same-origin under /api).
6
- # Set it only when hosting the frontend separately from the backend, e.g.:
 
 
 
 
7
  # VITE_API_URL=https://your-backend.example.com
 
1
  # Frontend build-time configuration.
2
 
3
+ # Base URL for the API. Resolution order at runtime:
4
+ # 1. HF Space variable `DOCUASK_API_URL` (set in the Static Space settings) β€”
5
+ # preferred for the Hugging Face deploy; changeable without a rebuild.
6
+ # 2. VITE_API_URL below (baked in at build time).
7
+ # 3. "/api" β€” local dev (Vite proxy) and the single-origin Docker image.
8
+ #
9
+ # Leave UNSET for local dev and the combined image. For split hosting where you
10
+ # want the URL baked in instead of using the HF variable, set it here:
11
  # VITE_API_URL=https://your-backend.example.com
frontend/src/api.js CHANGED
@@ -1,6 +1,20 @@
1
- // In dev, requests go to "/api/*" and Vite proxies them to the backend.
2
- // In production, set VITE_API_URL to the deployed backend origin.
3
- export const API_BASE = import.meta.env.VITE_API_URL || "/api";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  /** Extract a human-readable message from a FastAPI error response. */
6
  async function errorDetail(res) {
 
1
+ // Resolve where the API lives, in priority order:
2
+ // 1. An HF Space variable `DOCUASK_API_URL` (read at runtime β€” set it in the
3
+ // Static Space settings so the URL can change without a rebuild).
4
+ // 2. A build-time `VITE_API_URL` (baked in via frontend/.env.production).
5
+ // 3. "/api" β€” local dev (Vite proxy) and the combined single-origin image.
6
+ function resolveApiBase() {
7
+ if (typeof window !== "undefined") {
8
+ const fromHf = window.huggingface?.variables?.DOCUASK_API_URL;
9
+ if (fromHf) return fromHf.replace(/\/$/, "");
10
+ }
11
+ if (import.meta.env.VITE_API_URL) {
12
+ return import.meta.env.VITE_API_URL.replace(/\/$/, "");
13
+ }
14
+ return "/api";
15
+ }
16
+
17
+ export const API_BASE = resolveApiBase();
18
 
19
  /** Extract a human-readable message from a FastAPI error response. */
20
  async function errorDetail(res) {