Switch deploy to HF Static Space (frontend) + separate API
Browse filesHugging 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 +3 -3
- PROJECT_SPEC.md +4 -3
- README.md +43 -21
- backend/Dockerfile +3 -1
- backend/Procfile +1 -0
- backend/app/main.py +7 -3
- frontend/.env.example +8 -4
- frontend/src/api.js +17 -3
|
@@ -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
|
| 4 |
-
# demo tracks main. The Space builds the
|
| 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
|
|
@@ -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
|
| 123 |
-
>
|
| 124 |
-
>
|
|
|
|
| 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 |
|
|
@@ -3,8 +3,9 @@ title: DocuAsk
|
|
| 3 |
emoji: π
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: gray
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
| 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
|
| 98 |
|
| 99 |
-
The
|
| 100 |
-
|
| 101 |
-
|
| 102 |
|
| 103 |
-
1.
|
| 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 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
|
| 114 |
-
|
| 115 |
-
[`sync-to-hf`](./.github/workflows/sync-to-hf.yml)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
>
|
| 126 |
-
> `
|
| 127 |
-
>
|
|
|
|
| 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:**
|
| 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 |
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
| 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}"]
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: uvicorn app.main:app --host 0.0.0.0 --port $PORT
|
|
@@ -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 |
-
#
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(",")
|
|
@@ -1,7 +1,11 @@
|
|
| 1 |
# Frontend build-time configuration.
|
| 2 |
|
| 3 |
-
# Base URL for the API.
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -1,6 +1,20 @@
|
|
| 1 |
-
//
|
| 2 |
-
//
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) {
|