@caddis/cli 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -1
- package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
- package/bundles/antigravity-plugin/agents/preflight.md +53 -0
- package/bundles/antigravity-plugin/guard_agy.py +338 -0
- package/bundles/antigravity-plugin/hooks.json +36 -0
- package/bundles/antigravity-plugin/mcp_config.json +8 -0
- package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
- package/bundles/antigravity-plugin/plugin.json +5 -0
- package/bundles/antigravity-plugin/session_end_agy.py +57 -0
- package/bundles/antigravity-plugin/skills/_registry.md +115 -0
- package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
- package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
- package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
- package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
- package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
- package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
- package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
- package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
- package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
- package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
- package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
- package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
- package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
- package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
- package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
- package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
- package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
- package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
- package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
- package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
- package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
- package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
- package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
- package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
- package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
- package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
- package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
- package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
- package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
- package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
- package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
- package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
- package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
- package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
- package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
- package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
- package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
- package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
- package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
- package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
- package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
- package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
- package/bundles/manifest.json +6 -0
- package/dist/cli.js +5363 -0
- package/package.json +61 -4
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: windows-deployment
|
|
3
|
+
description: "Deploy FastAPI + React/Vite apps to Windows Server with NSSM services, reverse proxy (IIS or nginx), and git-pull workflow. Use when deploying any web app to a Windows prod server, setting up NSSM services, configuring IIS or nginx reverse proxy, making code environment-aware for dev/prod, or troubleshooting prod deployment issues."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Windows Server Deployment Skill
|
|
7
|
+
|
|
8
|
+
Deploy FastAPI + React/Vite web applications to Windows Server using NSSM services, a reverse proxy (IIS or nginx), and a git-pull workflow.
|
|
9
|
+
|
|
10
|
+
## 1. When to Apply This Skill
|
|
11
|
+
|
|
12
|
+
**Trigger conditions:**
|
|
13
|
+
- "Deploy this app to prod" / "Deploy to Windows Server"
|
|
14
|
+
- Setting up a new web application on a Windows prod server
|
|
15
|
+
- Adding a reverse proxy (IIS or nginx) for an app
|
|
16
|
+
- Registering NSSM services
|
|
17
|
+
- Configuring environment-aware dev/prod code (subpath, auth, static serving)
|
|
18
|
+
- Troubleshooting prod deployment (502, 404, service won't start)
|
|
19
|
+
- "Why can't the frontend reach the API?"
|
|
20
|
+
|
|
21
|
+
**Prerequisites the user should have:**
|
|
22
|
+
- A FastAPI + React/Vite app
|
|
23
|
+
- A Windows Server target machine
|
|
24
|
+
- NSSM installed on the target
|
|
25
|
+
- A git remote (Gitea, GitHub, etc.) accessible from both dev and prod
|
|
26
|
+
- Python and Node.js installed on the target
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Architecture Overview
|
|
31
|
+
|
|
32
|
+
Two reverse proxy strategies are available. Choose based on what the server already runs.
|
|
33
|
+
|
|
34
|
+
### Strategy A — IIS (Recommended When IIS Is Already Running)
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Browser → IIS (:80/:443)
|
|
38
|
+
├─ Windows Auth + Active Directory (handled by IIS)
|
|
39
|
+
├─ URL Rewrite: /{subpath}/api/* → http://localhost:{api-port}/api/*
|
|
40
|
+
└─ URL Rewrite: /{subpath}/* → http://localhost:{api-port}/*
|
|
41
|
+
├─ /api/* → FastAPI routers
|
|
42
|
+
└─ /* → FastAPI serves dist/ static files
|
|
43
|
+
|
|
44
|
+
NSSM manages:
|
|
45
|
+
- <org>-{app}-api-prod (one per app)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Key characteristics:**
|
|
49
|
+
- IIS handles authentication (Windows Auth, AD groups) — app code has zero auth logic
|
|
50
|
+
- App is deployed under a subpath (e.g. `/my-app/`) — IIS strips the prefix before forwarding
|
|
51
|
+
- FastAPI serves both API routes and static frontend files in production
|
|
52
|
+
- No separate frontend service/port needed
|
|
53
|
+
|
|
54
|
+
### Strategy B — nginx (Standalone Reverse Proxy)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Browser → nginx (:51xx) → serves static dist/ files
|
|
58
|
+
→ proxies /api/* → uvicorn (:81xx)
|
|
59
|
+
|
|
60
|
+
NSSM manages:
|
|
61
|
+
- <org>-nginx-prod (single instance, all apps)
|
|
62
|
+
- <org>-{app}-api-prod (one per app)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Key characteristics:**
|
|
66
|
+
- nginx handles static file serving and API proxying
|
|
67
|
+
- Each app gets its own port (e.g. `:5103`, `:5104`)
|
|
68
|
+
- No built-in auth — add separately if needed
|
|
69
|
+
- Clean separation: nginx = static + proxy, uvicorn = API only
|
|
70
|
+
|
|
71
|
+
### Decision Matrix
|
|
72
|
+
|
|
73
|
+
| Factor | Choose IIS | Choose nginx |
|
|
74
|
+
|--------|-----------|-------------|
|
|
75
|
+
| IIS already running on the server | ✅ | |
|
|
76
|
+
| Need Windows Authentication / AD | ✅ | |
|
|
77
|
+
| Apps must be on port 80/443 | ✅ | |
|
|
78
|
+
| Multiple apps under one domain with subpaths | ✅ | |
|
|
79
|
+
| No IIS on the server | | ✅ |
|
|
80
|
+
| Need lightweight, portable setup | | ✅ |
|
|
81
|
+
| Each app gets its own port | | ✅ |
|
|
82
|
+
|
|
83
|
+
### Port Convention
|
|
84
|
+
|
|
85
|
+
| Environment | API (uvicorn) | Frontend |
|
|
86
|
+
|-------------|--------------|----------|
|
|
87
|
+
| Dev | 81x2 | 51x2 (Vite dev server) |
|
|
88
|
+
| Prod (IIS) | 81x3 | IIS on :80/:443 (no separate FE port) |
|
|
89
|
+
| Prod (nginx)| 81x3 | 51x3 (nginx) |
|
|
90
|
+
|
|
91
|
+
Where `x` is the app number. Prod API ports increment per app on the shared server (8103, 8104, 8105...).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 3. Deployment Flow
|
|
96
|
+
|
|
97
|
+
### First-Time Setup (Both Strategies)
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
1. Clone repo on prod via git
|
|
101
|
+
2. Set up Python venv + install deps
|
|
102
|
+
3. Build frontend (npm ci && npm run build:prod)
|
|
103
|
+
4. Create config/.env.api.prod
|
|
104
|
+
5. Register NSSM API service
|
|
105
|
+
6. Configure reverse proxy (IIS or nginx)
|
|
106
|
+
7. Open firewall ports
|
|
107
|
+
8. Verify
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Subsequent Deploys
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
1. Dev: git push origin main
|
|
114
|
+
2. Prod: git pull
|
|
115
|
+
3. pip install -e . (if Python deps changed)
|
|
116
|
+
4. cd frontend && npm ci && npm run build:prod (if FE changed)
|
|
117
|
+
5. nssm restart <org>-{app}-api-prod
|
|
118
|
+
6. Verify
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3.1 Remote Capability Handshake (Required Before Saying "Can't Access Prod")
|
|
122
|
+
|
|
123
|
+
When a user asks for remote prod actions, do **not** assume remote access is unavailable.
|
|
124
|
+
|
|
125
|
+
Run this handshake first:
|
|
126
|
+
|
|
127
|
+
```powershell
|
|
128
|
+
$server = "{hostname}"
|
|
129
|
+
|
|
130
|
+
# 1) Reachability
|
|
131
|
+
Test-Connection -ComputerName $server -Count 1
|
|
132
|
+
|
|
133
|
+
# 2) WinRM availability
|
|
134
|
+
Test-WSMan -ComputerName $server
|
|
135
|
+
|
|
136
|
+
# 3) Actual command execution
|
|
137
|
+
Invoke-Command -ComputerName $server -ScriptBlock {
|
|
138
|
+
hostname
|
|
139
|
+
Get-Location
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Decision rule:
|
|
144
|
+
- If steps 1-3 succeed, proceed with remote deploy actions.
|
|
145
|
+
- If any step fails, report the exact failing step and provide fallback instructions.
|
|
146
|
+
|
|
147
|
+
**Hard rule:** Never claim "I can't remote into prod" until this probe fails in the current session.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. Common Steps (Both Strategies)
|
|
152
|
+
|
|
153
|
+
### 4.1 Git Setup on Prod
|
|
154
|
+
|
|
155
|
+
```powershell
|
|
156
|
+
cd G:\Projects
|
|
157
|
+
git clone http://{git-host}/{org}/{app-name}.git
|
|
158
|
+
cd {app-name}
|
|
159
|
+
|
|
160
|
+
# Credential storage (one-time)
|
|
161
|
+
git config credential.helper store
|
|
162
|
+
[System.Environment]::SetEnvironmentVariable("GCM_PROVIDER", "generic", "User")
|
|
163
|
+
|
|
164
|
+
git pull # enter credentials once, stored thereafter
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If using a self-hosted git server, ensure the `hosts` file maps the hostname.
|
|
168
|
+
|
|
169
|
+
### 4.2 Python Environment
|
|
170
|
+
|
|
171
|
+
```powershell
|
|
172
|
+
python -m venv .venv
|
|
173
|
+
.venv\Scripts\Activate.ps1
|
|
174
|
+
pip install -e .
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Verify Python version** matches dev: `python --version`.
|
|
178
|
+
|
|
179
|
+
### 4.3 Frontend Build
|
|
180
|
+
|
|
181
|
+
```powershell
|
|
182
|
+
cd frontend
|
|
183
|
+
npm ci
|
|
184
|
+
npm run build:prod # ← uses production Vite mode (see §7)
|
|
185
|
+
Get-ChildItem dist -Recurse | Measure-Object
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 4.4 Environment Config
|
|
189
|
+
|
|
190
|
+
Create `config/.env.api.prod`:
|
|
191
|
+
|
|
192
|
+
```env
|
|
193
|
+
APP_ENV=production
|
|
194
|
+
APP_PORT=81x3
|
|
195
|
+
# Add app-specific vars (DB connection, etc.)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Never commit real `.env` files.** Only `.env.example` with placeholder values.
|
|
199
|
+
|
|
200
|
+
### 4.5 Register API Service with NSSM
|
|
201
|
+
|
|
202
|
+
```powershell
|
|
203
|
+
$projectRoot = "G:\Projects\{app-name}"
|
|
204
|
+
$pythonExe = "$projectRoot\.venv\Scripts\python.exe"
|
|
205
|
+
$svcName = "<org>-{short}-api-prod"
|
|
206
|
+
$port = 81x3 # Use actual port
|
|
207
|
+
|
|
208
|
+
nssm install $svcName $pythonExe "-m uvicorn src.api.main:app --host 0.0.0.0 --port $port"
|
|
209
|
+
nssm set $svcName AppDirectory $projectRoot
|
|
210
|
+
nssm set $svcName DisplayName "{App Name} API (PROD)"
|
|
211
|
+
nssm set $svcName Description "{App Name} FastAPI backend - prod environment"
|
|
212
|
+
nssm set $svcName AppEnvironmentExtra "APP_ENV=production"
|
|
213
|
+
|
|
214
|
+
# Logging
|
|
215
|
+
nssm set $svcName AppStdout "$projectRoot\logs\$svcName-stdout.log"
|
|
216
|
+
nssm set $svcName AppStderr "$projectRoot\logs\$svcName-stderr.log"
|
|
217
|
+
nssm set $svcName AppRotateFiles 1
|
|
218
|
+
nssm set $svcName AppRotateBytes 10485760 # 10 MB
|
|
219
|
+
|
|
220
|
+
# Restart on failure
|
|
221
|
+
nssm set $svcName AppRestartDelay 5000
|
|
222
|
+
|
|
223
|
+
nssm start $svcName
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 5. Strategy A — IIS Reverse Proxy Setup
|
|
229
|
+
|
|
230
|
+
### 5.1 Prerequisites
|
|
231
|
+
|
|
232
|
+
Install IIS features (if not already present):
|
|
233
|
+
```powershell
|
|
234
|
+
Install-WindowsFeature Web-Server, Web-Mgmt-Console
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Install these IIS modules:
|
|
238
|
+
- **URL Rewrite** — https://www.iis.net/downloads/microsoft/url-rewrite
|
|
239
|
+
- **Application Request Routing (ARR)** — https://www.iis.net/downloads/microsoft/application-request-routing
|
|
240
|
+
|
|
241
|
+
Enable ARR proxy functionality:
|
|
242
|
+
```powershell
|
|
243
|
+
# In IIS Manager → Server level → Application Request Routing → Server Proxy Settings
|
|
244
|
+
# Check "Enable proxy" → Apply
|
|
245
|
+
# Or via appcmd:
|
|
246
|
+
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" set config -section:system.webServer/proxy /enabled:true /commit:apphost
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 5.2 IIS Rewrite Rules
|
|
250
|
+
|
|
251
|
+
Create rewrite rules under the Default Web Site (or a dedicated site). Two rules are needed — the API rule must come first:
|
|
252
|
+
|
|
253
|
+
**Rule 1 — API proxy (matches first):**
|
|
254
|
+
```xml
|
|
255
|
+
<rule name="{app-name}-api" stopProcessing="true">
|
|
256
|
+
<match url="^{subpath}/api/(.*)" />
|
|
257
|
+
<action type="Rewrite" url="http://localhost:{api-port}/api/{R:1}" />
|
|
258
|
+
</rule>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Rule 2 — Frontend catch-all:**
|
|
262
|
+
```xml
|
|
263
|
+
<rule name="{app-name}-frontend" stopProcessing="true">
|
|
264
|
+
<match url="^{subpath}/(.*)" />
|
|
265
|
+
<action type="Rewrite" url="http://localhost:{api-port}/{R:1}" />
|
|
266
|
+
</rule>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**How it works:** IIS strips the subpath prefix before forwarding. FastAPI receives clean paths (`/api/v1/...` and `/index.html`).
|
|
270
|
+
|
|
271
|
+
### 5.3 Windows Authentication
|
|
272
|
+
|
|
273
|
+
Enable Windows Auth on the IIS site/app:
|
|
274
|
+
|
|
275
|
+
```powershell
|
|
276
|
+
# Disable anonymous access, enable Windows Auth
|
|
277
|
+
Set-WebConfigurationProperty -Filter '/system.webServer/security/authentication/anonymousAuthentication' `
|
|
278
|
+
-PSPath 'IIS:\Sites\Default Web Site' -Name 'enabled' -Value 'false'
|
|
279
|
+
Set-WebConfigurationProperty -Filter '/system.webServer/security/authentication/windowsAuthentication' `
|
|
280
|
+
-PSPath 'IIS:\Sites\Default Web Site' -Name 'enabled' -Value 'true'
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Key insight:** Auth is fully external to the application. FastAPI endpoints have no auth dependencies or middleware — IIS rejects unauthenticated requests before they reach uvicorn.
|
|
284
|
+
|
|
285
|
+
### 5.4 FastAPI Static File Serving (Required for IIS Strategy)
|
|
286
|
+
|
|
287
|
+
With IIS, FastAPI must serve the frontend `dist/` files in production. Add this to `main.py` — gated on a settings flag so it only activates in production:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from pathlib import Path
|
|
291
|
+
from fastapi.responses import FileResponse
|
|
292
|
+
from fastapi.staticfiles import StaticFiles
|
|
293
|
+
|
|
294
|
+
_FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
|
295
|
+
|
|
296
|
+
# ... app setup, routers ...
|
|
297
|
+
|
|
298
|
+
# ── Static file serving (production only) ──
|
|
299
|
+
if not settings.debug and _FRONTEND_DIST.is_dir():
|
|
300
|
+
app.mount("/assets", StaticFiles(directory=_FRONTEND_DIST / "assets"), name="static")
|
|
301
|
+
|
|
302
|
+
@app.get("/{full_path:path}")
|
|
303
|
+
async def serve_spa(full_path: str) -> FileResponse:
|
|
304
|
+
# Path traversal guard (OWASP A04)
|
|
305
|
+
file = (_FRONTEND_DIST / full_path).resolve()
|
|
306
|
+
if file.is_file() and str(file).startswith(str(_FRONTEND_DIST)):
|
|
307
|
+
return FileResponse(file)
|
|
308
|
+
return FileResponse(_FRONTEND_DIST / "index.html")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Critical ordering rule:** Mount this AFTER all API routers. The SPA catch-all must be the last route.
|
|
312
|
+
|
|
313
|
+
**Dev behavior:** `settings.debug == True` → static block is skipped → Vite dev server handles frontend.
|
|
314
|
+
**Prod behavior:** `settings.debug == False` → FastAPI serves `dist/assets/*` and falls back to `index.html` for SPA routing.
|
|
315
|
+
|
|
316
|
+
### 5.5 Firewall (IIS)
|
|
317
|
+
|
|
318
|
+
No per-app firewall rules needed — IIS listens on ports 80/443, which are typically already open. API ports (81xx) stay on localhost.
|
|
319
|
+
|
|
320
|
+
### 5.6 Verify (IIS)
|
|
321
|
+
|
|
322
|
+
```powershell
|
|
323
|
+
# IIS site running?
|
|
324
|
+
Get-Website | Where-Object { $_.State -eq 'Started' }
|
|
325
|
+
|
|
326
|
+
# Frontend loads?
|
|
327
|
+
Invoke-WebRequest -Uri "http://localhost/{subpath}/" -UseBasicParsing -UseDefaultCredentials | Select-Object StatusCode
|
|
328
|
+
|
|
329
|
+
# API proxied?
|
|
330
|
+
(Invoke-WebRequest -Uri "http://localhost/{subpath}/api/v1/{endpoint}" -UseBasicParsing -UseDefaultCredentials).Content
|
|
331
|
+
|
|
332
|
+
# Health?
|
|
333
|
+
(Invoke-WebRequest -Uri "http://localhost/{subpath}/api/health" -UseBasicParsing -UseDefaultCredentials).Content
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## 6. Strategy B — nginx Reverse Proxy Setup
|
|
339
|
+
|
|
340
|
+
### 6.1 nginx as a Shared Service
|
|
341
|
+
|
|
342
|
+
nginx runs as a **single NSSM service** serving all apps. Each app gets its own `server {}` block on a different port.
|
|
343
|
+
|
|
344
|
+
```powershell
|
|
345
|
+
# First-time nginx setup
|
|
346
|
+
nssm install <org>-nginx-prod "C:\nginx\nginx.exe"
|
|
347
|
+
nssm set <org>-nginx-prod AppDirectory "C:\nginx"
|
|
348
|
+
nssm set <org>-nginx-prod DisplayName "nginx Reverse Proxy (PROD)"
|
|
349
|
+
nssm set <org>-nginx-prod AppStdout "G:\Projects\logs\<org>-nginx-prod-stdout.log"
|
|
350
|
+
nssm set <org>-nginx-prod AppStderr "G:\Projects\logs\<org>-nginx-prod-stderr.log"
|
|
351
|
+
nssm set <org>-nginx-prod AppRotateFiles 1
|
|
352
|
+
nssm set <org>-nginx-prod AppRotateBytes 10485760
|
|
353
|
+
nssm set <org>-nginx-prod AppRestartDelay 5000
|
|
354
|
+
|
|
355
|
+
nssm start <org>-nginx-prod
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 6.2 nginx Server Block Template
|
|
359
|
+
|
|
360
|
+
Add to `C:\nginx\conf\nginx.conf`:
|
|
361
|
+
|
|
362
|
+
```nginx
|
|
363
|
+
server {
|
|
364
|
+
listen {51x3};
|
|
365
|
+
server_name {hostname} localhost;
|
|
366
|
+
|
|
367
|
+
location /api/ {
|
|
368
|
+
proxy_pass http://127.0.0.1:{81x3}/api/;
|
|
369
|
+
proxy_http_version 1.1;
|
|
370
|
+
proxy_set_header Host $host;
|
|
371
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
372
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
373
|
+
proxy_read_timeout 120s;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
location /health {
|
|
377
|
+
proxy_pass http://127.0.0.1:{81x3}/health;
|
|
378
|
+
proxy_http_version 1.1;
|
|
379
|
+
proxy_set_header Host $host;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
location / {
|
|
383
|
+
root G:/Projects/{app-name}/frontend/dist;
|
|
384
|
+
try_files $uri $uri/ /index.html;
|
|
385
|
+
expires 1h;
|
|
386
|
+
add_header Cache-Control "public, no-transform";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Test and reload:
|
|
392
|
+
```powershell
|
|
393
|
+
C:\nginx\nginx.exe -t -c C:\nginx\conf\nginx.conf
|
|
394
|
+
nssm restart <org>-nginx-prod
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### 6.3 Firewall (nginx)
|
|
398
|
+
|
|
399
|
+
```powershell
|
|
400
|
+
# Only expose the FE port — API stays on localhost
|
|
401
|
+
New-NetFirewallRule -DisplayName "{App Name} ({51x3})" -Direction Inbound -LocalPort {51x3} -Protocol TCP -Action Allow
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 6.4 CORS Configuration (nginx only)
|
|
405
|
+
|
|
406
|
+
When nginx serves on a different port than the API, CORS is needed:
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
app.add_middleware(
|
|
410
|
+
CORSMiddleware,
|
|
411
|
+
allow_origins=[
|
|
412
|
+
"http://localhost:5102", # Vite dev
|
|
413
|
+
"http://localhost:51x3", # nginx prod
|
|
414
|
+
],
|
|
415
|
+
allow_credentials=True,
|
|
416
|
+
allow_methods=["*"],
|
|
417
|
+
allow_headers=["*"],
|
|
418
|
+
)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**IIS note:** CORS is generally not needed with IIS because the browser hits IIS on the same origin as the page — no cross-origin request occurs.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## 7. Environment-Aware Code — The One-Codebase Pattern
|
|
426
|
+
|
|
427
|
+
The single most important deployment principle: **one codebase serves both dev and prod.** Environment differences are handled by **runtime/build-time detection**, not separate code.
|
|
428
|
+
|
|
429
|
+
### 7.1 Vite Config — Conditional Base Path
|
|
430
|
+
|
|
431
|
+
When deployed under a subpath (IIS strategy), asset URLs must include the subpath prefix. Vite bakes this in at build time via the `base` config.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// vite.config.ts
|
|
435
|
+
import { defineConfig } from "vite";
|
|
436
|
+
import react from "@vitejs/plugin-react";
|
|
437
|
+
|
|
438
|
+
export default defineConfig(({ mode }) => {
|
|
439
|
+
const isProd = mode === "production";
|
|
440
|
+
const base = isProd ? "/{subpath}/" : "/";
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
base,
|
|
444
|
+
plugins: [react()],
|
|
445
|
+
server: {
|
|
446
|
+
port: 5102,
|
|
447
|
+
proxy: isProd ? undefined : {
|
|
448
|
+
"/api": {
|
|
449
|
+
target: "http://localhost:8102",
|
|
450
|
+
changeOrigin: true,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Dev:** `base: "/"`, proxy active → Vite dev server handles everything.
|
|
459
|
+
**Prod build:** `base: "/{subpath}/"` → all asset paths in `dist/index.html` prefixed correctly.
|
|
460
|
+
|
|
461
|
+
If not deploying under a subpath (nginx or root path), keep `base: "/"` for both modes and simplify.
|
|
462
|
+
|
|
463
|
+
### 7.2 Client-Side Router — basepath Configuration
|
|
464
|
+
|
|
465
|
+
**This is the most common cause of "Not Found" on subpath deployments.** The reverse proxy (IIS) strips the subpath before forwarding to FastAPI, but the browser URL still shows the subpath. Client-side routers need to know about this prefix.
|
|
466
|
+
|
|
467
|
+
#### TanStack Router
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// frontend/src/main.tsx
|
|
471
|
+
import { createRouter } from "@tanstack/react-router";
|
|
472
|
+
|
|
473
|
+
const router = createRouter({
|
|
474
|
+
routeTree,
|
|
475
|
+
basepath: import.meta.env.BASE_URL, // dev: "/", prod: "/{subpath}/"
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### React Router (v6+)
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
<BrowserRouter basename={import.meta.env.BASE_URL}>
|
|
483
|
+
<Routes>...</Routes>
|
|
484
|
+
</BrowserRouter>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
`import.meta.env.BASE_URL` is automatically set by Vite from the `base` config in `vite.config.ts` (§7.1). This is the single source of truth for the subpath prefix across the entire frontend.
|
|
488
|
+
|
|
489
|
+
**Why this breaks:** IIS rewrites `/my-app/dashboard` → `/dashboard` server-side. FastAPI serves `index.html`. But the browser URL bar still shows `/my-app/dashboard`. The client-side router sees that URL, and without `basepath`, tries to match `/my-app/dashboard` against your route tree — which only has `/dashboard`. No match → "Not Found".
|
|
490
|
+
|
|
491
|
+
### 7.3 Axios Client — Dynamic Base URL
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// frontend/src/api/client.ts
|
|
495
|
+
import axios from "axios";
|
|
496
|
+
|
|
497
|
+
const apiClient = axios.create({
|
|
498
|
+
baseURL: `${import.meta.env.BASE_URL}api/v1`,
|
|
499
|
+
withCredentials: true, // Required if using Windows Auth (IIS)
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
export default apiClient;
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`import.meta.env.BASE_URL` is set by Vite automatically from the `base` config:
|
|
506
|
+
- Dev: `"/"` → `baseURL = "/api/v1"` → Vite proxy forwards to FastAPI
|
|
507
|
+
- Prod (subpath): `"/{subpath}/"` → `baseURL = "/{subpath}/api/v1"` → IIS rewrites to FastAPI
|
|
508
|
+
|
|
509
|
+
**`withCredentials: true`** is required when the reverse proxy handles auth (e.g. IIS Windows Auth). With nginx or no auth, it's harmless.
|
|
510
|
+
|
|
511
|
+
### 7.4 Static Asset Fetches — The Hidden Subpath Trap
|
|
512
|
+
|
|
513
|
+
Router basepath (§7.2) and API baseURL (§7.3) are well-known subpath requirements. **Runtime fetches of files from the `public/` directory are easily missed** because they work fine in dev (where `BASE_URL` is `"/"`) and only break under a subpath.
|
|
514
|
+
|
|
515
|
+
Examples of static asset fetches that break under a subpath:
|
|
516
|
+
- `fetch("/config.json")` — app configuration
|
|
517
|
+
- `fetch("/ireland-counties.geojson")` — map data
|
|
518
|
+
- `fetch("/manifest.json")` — PWA manifest
|
|
519
|
+
- Any `fetch("/<file>")` for files in `public/`
|
|
520
|
+
|
|
521
|
+
**Fix:** Create a `toAppUrl()` helper and use it everywhere:
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// frontend/src/lib/utils.ts
|
|
525
|
+
export function toAppUrl(path: string): string {
|
|
526
|
+
const base = import.meta.env.BASE_URL ?? "/";
|
|
527
|
+
const stripped = path.startsWith("/") ? path.slice(1) : path;
|
|
528
|
+
return `${base}${stripped}`;
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Usage:
|
|
533
|
+
```typescript
|
|
534
|
+
// ❌ BAD: Root-absolute — breaks under /my-app/ subpath
|
|
535
|
+
const GEOJSON_PATH = "/ireland-counties.geojson";
|
|
536
|
+
fetch("/config.json");
|
|
537
|
+
|
|
538
|
+
// ✅ GOOD: Subpath-aware
|
|
539
|
+
import { toAppUrl } from "@/lib/utils";
|
|
540
|
+
const GEOJSON_PATH = toAppUrl("ireland-counties.geojson");
|
|
541
|
+
fetch(toAppUrl("config.json"));
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**Detection:** Search for root-absolute fetch patterns that bypass `BASE_URL`:
|
|
545
|
+
```bash
|
|
546
|
+
grep -rn 'fetch("/' frontend/src/
|
|
547
|
+
grep -rn '= "/[a-zA-Z]' frontend/src/
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**The three categories of subpath-sensitive URLs:**
|
|
551
|
+
|
|
552
|
+
| Category | What | Where to fix | Reference |
|
|
553
|
+
|----------|------|-------------|----------|
|
|
554
|
+
| Router basepath | Route matching in browser | `createRouter({ basepath })` | §7.2 |
|
|
555
|
+
| API baseURL | Backend API calls | `axios.create({ baseURL })` | §7.3 |
|
|
556
|
+
| Static asset fetches | `public/` files loaded at runtime | `toAppUrl()` helper | §7.4 |
|
|
557
|
+
|
|
558
|
+
All three must use `import.meta.env.BASE_URL`. Missing any one causes prod-only failures.
|
|
559
|
+
|
|
560
|
+
### 7.5 package.json Build Scripts
|
|
561
|
+
|
|
562
|
+
```json
|
|
563
|
+
{
|
|
564
|
+
"scripts": {
|
|
565
|
+
"dev": "vite",
|
|
566
|
+
"build": "tsc -b && vite build",
|
|
567
|
+
"build:prod": "tsc -b && vite build --mode production"
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- `npm run build` — default build (dev mode, `base: "/"`)
|
|
573
|
+
- `npm run build:prod` — production build with subpath prefix
|
|
574
|
+
|
|
575
|
+
### 7.6 FastAPI Settings — Debug Flag
|
|
576
|
+
|
|
577
|
+
Use a settings property to distinguish dev from prod behavior:
|
|
578
|
+
|
|
579
|
+
```python
|
|
580
|
+
# src/config/settings.py
|
|
581
|
+
from pydantic_settings import BaseSettings
|
|
582
|
+
|
|
583
|
+
class Settings(BaseSettings):
|
|
584
|
+
app_env: str = "development"
|
|
585
|
+
app_port: int = 8102
|
|
586
|
+
|
|
587
|
+
@property
|
|
588
|
+
def debug(self) -> bool:
|
|
589
|
+
return self.app_env == "development"
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
`config/.env.api.dev` → `APP_ENV=development` → `debug=True` → no static serving, docs enabled
|
|
593
|
+
`config/.env.api.prod` → `APP_ENV=production` → `debug=False` → static serving active, docs disabled
|
|
594
|
+
|
|
595
|
+
### 7.7 What This Means for Development
|
|
596
|
+
|
|
597
|
+
| Action | Dev | Prod |
|
|
598
|
+
|--------|-----|------|
|
|
599
|
+
| Frontend served by | Vite dev server (`:5102`) | FastAPI (IIS) or nginx |
|
|
600
|
+
| API requests proxied by | Vite `proxy` config | IIS URL Rewrite or nginx |
|
|
601
|
+
| Auth | None | IIS Windows Auth (external) |
|
|
602
|
+
| `npm run build` needed? | No (Vite serves source) | Yes (`build:prod`) |
|
|
603
|
+
| FastAPI serves `dist/`? | No (`debug=True`) | Yes (IIS strategy) or No (nginx strategy) |
|
|
604
|
+
|
|
605
|
+
**All behavior switches automatically based on environment config.** You never maintain separate codebases.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## 8. NSSM Script (Reusable)
|
|
610
|
+
|
|
611
|
+
Projects can include a reusable NSSM install script:
|
|
612
|
+
|
|
613
|
+
```powershell
|
|
614
|
+
# Usage:
|
|
615
|
+
# .\nssm-install.ps1 -ProjectShort nps -Port 8103
|
|
616
|
+
param(
|
|
617
|
+
[Parameter(Mandatory)][string]$ProjectShort,
|
|
618
|
+
[Parameter(Mandatory)][int]$Port,
|
|
619
|
+
[string]$ProjectRoot = (Get-Location).Path
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
$svcName = "<org>-$ProjectShort-api-prod"
|
|
623
|
+
$pythonExe = "$ProjectRoot\.venv\Scripts\python.exe"
|
|
624
|
+
|
|
625
|
+
nssm install $svcName $pythonExe "-m uvicorn src.api.main:app --host 0.0.0.0 --port $Port"
|
|
626
|
+
nssm set $svcName AppDirectory $ProjectRoot
|
|
627
|
+
nssm set $svcName AppEnvironmentExtra "APP_ENV=production"
|
|
628
|
+
nssm set $svcName AppStdout "$ProjectRoot\logs\$svcName-stdout.log"
|
|
629
|
+
nssm set $svcName AppStderr "$ProjectRoot\logs\$svcName-stderr.log"
|
|
630
|
+
nssm set $svcName AppRotateFiles 1
|
|
631
|
+
nssm set $svcName AppRotateBytes 10485760
|
|
632
|
+
nssm set $svcName AppRestartDelay 5000
|
|
633
|
+
|
|
634
|
+
nssm start $svcName
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## 9. Troubleshooting
|
|
640
|
+
|
|
641
|
+
| Symptom | Cause | Fix |
|
|
642
|
+
|---------|-------|-----|
|
|
643
|
+
| "Unable to load periods" / API 404 | Frontend hitting static server, no proxy to API | Configure reverse proxy (IIS §5 or nginx §6) |
|
|
644
|
+
| Assets load but API calls fail on subpath | `vite.config.ts` not setting `base` for prod | Use `defineConfig(({ mode }) => ...)` pattern (§7.1) |
|
|
645
|
+
| Site works on prod but breaks after `git pull` | Sysadmin made manual edits — overwritten by pull | Make code environment-aware (§7), never hand-edit on prod |
|
|
646
|
+
| Next `npm run build` breaks prod | Build uses wrong mode (no subpath prefix) | Use `npm run build:prod` with `--mode production` (§7.3) |
|
|
647
|
+
| 502 Bad Gateway (IIS or nginx) | uvicorn not running | `Get-Service <org>-{short}-api-prod` → restart |
|
|
648
|
+
| IIS returns 401 Unauthorized | Windows Auth blocking — user not in AD | Check IIS auth settings, verify user is in allowed AD group |
|
|
649
|
+
| IIS returns 404 on subpath routes | URL Rewrite rules missing or wrong order | API rule must come before frontend catch-all (§5.2) |
|
|
650
|
+
| `bind() failed` on nginx start | Port already in use | `netstat -ano \| findstr {port}` → stop conflicting process |
|
|
651
|
+
| Stale frontend after `git pull` | `dist/` not rebuilt | `cd frontend && npm ci && npm run build:prod` |
|
|
652
|
+
| `ENOTEMPTY: rmdir dist/assets` | Process has `dist/` files locked | Stop service → build → restart |
|
|
653
|
+
| Service starts then stops immediately | Bad path or missing dependency | Check NSSM stderr log: `Get-Content logs\<org>-{short}-api-prod-stderr.log -Tail 50` |
|
|
654
|
+
| `git pull` fails with auth error | Credentials expired or GCM_PROVIDER not set | `git config credential.helper store` + `GCM_PROVIDER=generic` as user env var |
|
|
655
|
+
| Unclear whether remote control works | Assumed lack of access without probing | Run Remote Capability Handshake (§3.1) before deciding |
|
|
656
|
+
| Frontend shows blank page | SPA routing broken (no catch-all) | IIS: check FastAPI SPA catch-all (§5.4). nginx: check `try_files` (§6.2) |
|
|
657
|
+
| Frontend shows blank page (assets 404) | Vite `base` flattened to `"/"` under subpath | Restore `base: mode === "production" ? "/{subpath}/" : "/"` in `vite.config.ts`, rebuild, add deploy guard (§12) |
|
|
658
|
+
| Users see old version after deploy | Browser caching stale `index.html` | Add `Cache-Control: no-cache, no-store, must-revalidate` on HTML responses (§13) |
|
|
659
|
+
| Frontend shows "Not Found" on subpath | Client-side router missing `basepath` | Add `basepath: import.meta.env.BASE_URL` to router config (§7.2) |
|
|
660
|
+
| `Unexpected token '<'` parsing JSON | Static asset fetch uses root-absolute path (`/file.json`) under subpath | Use `toAppUrl()` helper for all `public/` file fetches (§7.4) |
|
|
661
|
+
| `withCredentials` CORS error in browser | Missing `allow_credentials=True` or wrong origin | Add CORS config (§6.4) or verify same-origin (IIS usually doesn't need CORS) |
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 10. Deploy Checklists (Copy-Paste)
|
|
666
|
+
|
|
667
|
+
### First-Time Deploy — IIS Strategy
|
|
668
|
+
|
|
669
|
+
```
|
|
670
|
+
- [ ] Git clone on prod + credential setup
|
|
671
|
+
- [ ] Python venv + pip install -e .
|
|
672
|
+
- [ ] config/.env.api.prod created (APP_ENV=production)
|
|
673
|
+
- [ ] Frontend: npm ci && npm run build:prod
|
|
674
|
+
- [ ] NSSM: API service registered and started
|
|
675
|
+
- [ ] IIS: URL Rewrite + ARR installed and proxy enabled
|
|
676
|
+
- [ ] IIS: Two rewrite rules added (API first, then frontend catch-all)
|
|
677
|
+
- [ ] IIS: Windows Auth enabled, anonymous disabled (if needed)
|
|
678
|
+
- [ ] FastAPI main.py has conditional static serving block (§5.4)
|
|
679
|
+
- [ ] vite.config.ts uses defineConfig(({ mode }) => ...) with conditional base (§7.1)
|
|
680
|
+
- [ ] client.ts uses import.meta.env.BASE_URL for baseURL (§7.3)
|
|
681
|
+
- [ ] Client-side router has basepath set to import.meta.env.BASE_URL (§7.2)
|
|
682
|
+
- [ ] All `fetch("/...")` calls for public/ assets use `toAppUrl()` helper (§7.4)
|
|
683
|
+
- [ ] Grep for root-absolute fetches: `grep -rn 'fetch("/' frontend/src/` returns no matches
|
|
684
|
+
- [ ] Deploy guard: post-build HTML check for `/{subpath}/assets/` prefix (§12.1)
|
|
685
|
+
- [ ] Cache-Control: `no-cache` on HTML, `immutable` on hashed assets (§13)
|
|
686
|
+
- [ ] Verify: frontend loads, API responds, auth works
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### First-Time Deploy — nginx Strategy
|
|
690
|
+
|
|
691
|
+
```
|
|
692
|
+
- [ ] Git clone on prod + credential setup
|
|
693
|
+
- [ ] Python venv + pip install -e .
|
|
694
|
+
- [ ] config/.env.api.prod created (APP_ENV=production)
|
|
695
|
+
- [ ] Frontend: npm ci && npm run build
|
|
696
|
+
- [ ] NSSM: API service registered and started
|
|
697
|
+
- [ ] nginx: server block added, tested, reloaded
|
|
698
|
+
- [ ] Firewall: FE port opened (API port NOT exposed)
|
|
699
|
+
- [ ] CORS: prod FE port added to FastAPI allow_origins
|
|
700
|
+
- [ ] Verify: static files, API proxy, health all return 200
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Subsequent Deploy (Both Strategies)
|
|
704
|
+
|
|
705
|
+
```
|
|
706
|
+
- [ ] Remote Capability Handshake run (ping + WSMan + Invoke-Command) (§3.1)
|
|
707
|
+
- [ ] git pull on prod
|
|
708
|
+
- [ ] pip install -e . (if deps changed)
|
|
709
|
+
- [ ] cd frontend && npm ci && npm run build:prod (if FE changed, IIS strategy)
|
|
710
|
+
OR npm run build (nginx strategy)
|
|
711
|
+
- [ ] nssm restart <org>-{short}-api-prod
|
|
712
|
+
- [ ] Verify app loads and API responds
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## 11. Lessons Learned
|
|
718
|
+
|
|
719
|
+
Hard-won lessons from real deployments:
|
|
720
|
+
|
|
721
|
+
1. **One codebase, one repo.** Dev and prod share identical code. Environment differences are handled by build-time mode (`vite --mode production`) and runtime config (`APP_ENV`). Never maintain separate dev/prod codebases.
|
|
722
|
+
|
|
723
|
+
2. **Auth is infrastructure, not application code.** When IIS handles Windows Auth, FastAPI endpoints have zero auth dependencies. IIS rejects unauthenticated requests before they reach uvicorn. Don't add auth middleware to match prod — it's not needed.
|
|
724
|
+
|
|
725
|
+
3. **`serve` cannot proxy API calls.** Vercel's `serve` is static-only. `/api/v1/...` requests hit serve → 404. You need a reverse proxy (IIS or nginx) or FastAPI must serve static files. Never use `serve` for apps with API backends.
|
|
726
|
+
|
|
727
|
+
4. **Sysadmin hand-edits on prod cause `git pull` disasters.** If a sysadmin manually edits `vite.config.ts` or `client.ts` on the prod server, the next `git pull` will overwrite those changes and break the site. Make the code environment-aware in the repo so `git pull` is always safe.
|
|
728
|
+
|
|
729
|
+
5. **Vite is a BUILD tool, not a runtime.** `vite.config.ts` is read at `npm run build` time. The `base` config gets baked into every asset URL in `dist/index.html`. The file doesn't exist in prod at runtime. If the `base` is wrong at build time, the site breaks — even though the config "looks right" in the repo.
|
|
730
|
+
|
|
731
|
+
6. **Rebuild frontend after `git pull`.** `git pull` updates source files, not the compiled `dist/` folder. Always run the appropriate build command after pulling frontend changes.
|
|
732
|
+
|
|
733
|
+
7. **IIS subpath deployment eliminates the need for a separate FE port.** With IIS serving on :80/:443 and rewriting to FastAPI, there is no port 5103 or nginx. One entry point, one port.
|
|
734
|
+
|
|
735
|
+
8. **API ports stay on localhost.** Only expose the entry-point ports (80/443 for IIS, 51xx for nginx). API ports (81xx) are proxied internally — no external access needed.
|
|
736
|
+
|
|
737
|
+
9. **`withCredentials: true` in axios is needed for IIS Windows Auth.** Without it, the browser won't send Negotiate/NTLM credentials in the API requests. It's harmless when no auth is used.
|
|
738
|
+
|
|
739
|
+
10. **Check the NSSM stderr log first** when a service won't start: `Get-Content logs\<org>-{short}-api-prod-stderr.log -Tail 50`.
|
|
740
|
+
|
|
741
|
+
11. **`GCM_PROVIDER=generic` is required** on Windows Server for git credential storage to work with self-hosted git servers. Set as a persistent user env var.
|
|
742
|
+
|
|
743
|
+
12. **Port conflicts between apps.** When multiple apps share a prod server, API backends must use different ports. Follow a consistent convention: 8103, 8104, 8105, etc.
|
|
744
|
+
|
|
745
|
+
13. **Client-side router `basepath` is mandatory for subpath deployments.** When IIS serves an app under `/my-app/`, the browser URL retains that prefix. IIS strips it server-side before forwarding to FastAPI, so FastAPI never sees it. But the client-side router runs in the *browser*, where the URL still has `/my-app/`. Without `basepath` (TanStack Router) or `basename` (React Router), the router can't match any route → "Not Found". This is the #1 gotcha in subpath deployments.
|
|
746
|
+
|
|
747
|
+
14. **Path traversal guard on SPA catch-all.** The `serve_spa` endpoint resolves file paths from user-supplied URL segments. Always `.resolve()` the path and verify it starts with the `dist/` directory to prevent path traversal attacks (OWASP A04).
|
|
748
|
+
|
|
749
|
+
15. **Static asset fetches are the hidden third subpath trap.** Router basepath and API baseURL are well-documented. Runtime `fetch("/config.json")` or `fetch("/data.geojson")` from `public/` are not — they work in dev (base is `/`) and silently break in prod under a subpath. The symptom is `Unexpected token '<'` because the root-absolute URL hits the wrong app or fallback page, returning HTML instead of JSON. Prevention: create a `toAppUrl()` helper and audit with `grep -rn 'fetch("/' frontend/src/`.
|
|
750
|
+
|
|
751
|
+
16. **Probe remote capability before declaring no access.** In Windows environments, agents may be able to execute remote commands via WinRM (`Invoke-Command`) even when interactive RDP control is not available. Always run the Remote Capability Handshake (§3.1) before stating remote access is unavailable.
|
|
752
|
+
|
|
753
|
+
17. **Never flatten `base` to `"/"` when the app is deployed under a subpath.** The Vite `base` config controls every `<script src>` and `<link href>` in the built `index.html`. Changing `base: mode === "production" ? "/{subpath}/" : "/"` → `base: "/"` causes all asset URLs to lose the subpath prefix. If the server has a catch-all rewrite rule (e.g. a Streamlit proxy at root), those asset requests hit the wrong backend → blank page. This is a silent, catastrophic break that is invisible in dev because dev always uses `base: "/"`.
|
|
754
|
+
|
|
755
|
+
18. **Deploy guards must validate built HTML before restarting services.** A wrong `base` path produces a valid build (exit code 0) but broken output. The deploy script must check the built `index.html` for the expected asset prefix **before** restarting services. If the check fails, the old working version stays live — the deploy aborts without downtime. See §12.
|
|
756
|
+
|
|
757
|
+
19. **Cache-Control headers prevent stale HTML after deploys.** Vite hashes asset filenames (`index-Crzj2IZk.js`) so old assets never collide with new ones. But `index.html` has a fixed path — without `no-cache`, browsers serve the old HTML (with old asset hashes) even after a deploy. Set `Cache-Control: no-cache, no-store, must-revalidate` on HTML and `public, max-age=31536000, immutable` on hashed assets. See §13.
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 12. Deploy Guards — Post-Build Validation
|
|
762
|
+
|
|
763
|
+
A deploy guard is a check that runs **after the build but before the service restart**. If the guard fails, the deploy aborts and the currently-running (working) version stays live. This prevents broken builds from causing downtime.
|
|
764
|
+
|
|
765
|
+
### 12.1 Base Path Guard (IIS Strategy — Required)
|
|
766
|
+
|
|
767
|
+
When deploying under a subpath, verify the built HTML references the correct asset prefix:
|
|
768
|
+
|
|
769
|
+
```powershell
|
|
770
|
+
# In deploy.ps1 — after vite build, before nssm restart
|
|
771
|
+
$indexHtml = Join-Path $frontendDir "dist\index.html"
|
|
772
|
+
if (Test-Path $indexHtml) {
|
|
773
|
+
$html = Get-Content $indexHtml -Raw
|
|
774
|
+
if ($html -notmatch '/{subpath}/assets/') {
|
|
775
|
+
throw "DEPLOY GUARD FAILED: Built index.html does not reference /{subpath}/assets/. " +
|
|
776
|
+
"The Vite base path is wrong — aborting deploy before service restart."
|
|
777
|
+
}
|
|
778
|
+
Write-Host "Deploy guard passed: assets reference /{subpath}/"
|
|
779
|
+
} else {
|
|
780
|
+
throw "DEPLOY GUARD FAILED: $indexHtml not found after build"
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Why this matters:** A wrong `base` in `vite.config.ts` produces a successful build (exit 0) but broken output. Without this guard, the deploy script would restart the service, serve the broken HTML, and take the app down. With the guard, the old working version stays live.
|
|
785
|
+
|
|
786
|
+
### 12.2 CI Build Validation (Belt-and-Suspenders)
|
|
787
|
+
|
|
788
|
+
The same check should run in CI so the broken build is caught **before** it reaches the deploy step:
|
|
789
|
+
|
|
790
|
+
```yaml
|
|
791
|
+
# In CI workflow — after frontend build step
|
|
792
|
+
- name: Validate production base path
|
|
793
|
+
shell: powershell
|
|
794
|
+
working-directory: frontend
|
|
795
|
+
run: |
|
|
796
|
+
$html = Get-Content dist\index.html -Raw
|
|
797
|
+
if ($html -notmatch '/{subpath}/assets/') {
|
|
798
|
+
throw "DEPLOY GUARD: Built index.html does not reference /{subpath}/assets/. " +
|
|
799
|
+
"Check vite.config.ts base setting."
|
|
800
|
+
}
|
|
801
|
+
Write-Host 'Base-path guard passed'
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Key:** Build with `--mode production` in CI (not just `vite build`), otherwise the mode-conditional `base` won't activate and the guard will always fail.
|
|
805
|
+
|
|
806
|
+
### 12.3 Additional Deploy Guards (Optional)
|
|
807
|
+
|
|
808
|
+
| Guard | What it checks | When to add |
|
|
809
|
+
|-------|---------------|-------------|
|
|
810
|
+
| **Health endpoint** | API responds 200 after restart | Always |
|
|
811
|
+
| **Base path** | Asset prefix in built HTML | Subpath deployments |
|
|
812
|
+
| **Dist size** | `dist/` is non-empty and reasonable size | Prevent empty-build deploys |
|
|
813
|
+
| **Git SHA convergence** | prod HEAD matches expected commit | Prevent stale-code deploys |
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## 13. Cache-Control Headers for SPA Serving
|
|
818
|
+
|
|
819
|
+
When FastAPI serves the built frontend (IIS strategy, §5.4), set appropriate cache headers to ensure browsers always get fresh HTML after deploys while caching hashed assets aggressively.
|
|
820
|
+
|
|
821
|
+
### 13.1 The Problem
|
|
822
|
+
|
|
823
|
+
Vite produces content-hashed asset filenames (`index-Crzj2IZk.js`), so old and new assets never collide. But `index.html` has a **fixed path** — without cache-control headers, browsers may serve a cached `index.html` with stale asset references after a deploy. The user sees a blank page or JS errors because the old hashed files no longer exist.
|
|
824
|
+
|
|
825
|
+
### 13.2 The Fix — Two Cache Policies
|
|
826
|
+
|
|
827
|
+
```python
|
|
828
|
+
from fastapi.responses import FileResponse
|
|
829
|
+
from fastapi.staticfiles import StaticFiles
|
|
830
|
+
|
|
831
|
+
# Cache policies
|
|
832
|
+
_NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
|
|
833
|
+
_LONG_CACHE = {"Cache-Control": "public, max-age=31536000, immutable"}
|
|
834
|
+
|
|
835
|
+
_FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
|
836
|
+
|
|
837
|
+
if not settings.debug and _FRONTEND_DIST.is_dir():
|
|
838
|
+
app.mount("/assets", StaticFiles(directory=_FRONTEND_DIST / "assets"), name="static")
|
|
839
|
+
|
|
840
|
+
@app.get("/{full_path:path}")
|
|
841
|
+
async def serve_spa(full_path: str) -> FileResponse:
|
|
842
|
+
file = (_FRONTEND_DIST / full_path).resolve()
|
|
843
|
+
if file.is_file() and str(file).startswith(str(_FRONTEND_DIST)):
|
|
844
|
+
# Hashed assets → cache forever; everything else → no cache
|
|
845
|
+
headers = _LONG_CACHE if "/assets/" in full_path else _NO_CACHE
|
|
846
|
+
return FileResponse(file, headers=headers)
|
|
847
|
+
return FileResponse(_FRONTEND_DIST / "index.html", headers=_NO_CACHE)
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### 13.3 Cache Policy Summary
|
|
851
|
+
|
|
852
|
+
| Resource | Cache-Control | Why |
|
|
853
|
+
|----------|--------------|-----|
|
|
854
|
+
| `index.html` (SPA shell) | `no-cache, no-store, must-revalidate` | Must always fetch latest — contains hashed asset references |
|
|
855
|
+
| `/assets/*.js`, `/assets/*.css` | `public, max-age=31536000, immutable` | Content-hashed filenames — safe to cache forever |
|
|
856
|
+
| Other static files (`favicon.ico`, etc.) | `no-cache, no-store, must-revalidate` | No content hash — may change between deploys |
|
|
857
|
+
|
|
858
|
+
### 13.4 nginx Equivalent
|
|
859
|
+
|
|
860
|
+
For the nginx strategy (§6.2), set the same headers in the server block:
|
|
861
|
+
|
|
862
|
+
```nginx
|
|
863
|
+
location / {
|
|
864
|
+
root G:/Projects/{app-name}/frontend/dist;
|
|
865
|
+
try_files $uri $uri/ /index.html;
|
|
866
|
+
|
|
867
|
+
# SPA shell — never cache
|
|
868
|
+
location = /index.html {
|
|
869
|
+
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
870
|
+
add_header Pragma "no-cache";
|
|
871
|
+
add_header Expires "0";
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
# Hashed assets — cache forever
|
|
875
|
+
location /assets/ {
|
|
876
|
+
expires 1y;
|
|
877
|
+
add_header Cache-Control "public, immutable";
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
```
|