Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d621b211b0 |
@@ -1,85 +0,0 @@
|
||||
# Gitea Actions: Aggregate openapi.yaml specs + trigger docs-site build
|
||||
# Closes leeworks-agents/api-company#10
|
||||
|
||||
name: Build Docs Site
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Re-build daily at 02:00 UTC to pick up spec changes
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
aggregate-specs:
|
||||
name: Aggregate OpenAPI Specs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout api-company
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: api-company
|
||||
|
||||
- name: Checkout zip-enrichment
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: leeworks-agents/zip-enrichment
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
path: zip-enrichment
|
||||
|
||||
- name: Checkout holidays
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: leeworks-agents/holidays
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
path: holidays
|
||||
|
||||
- name: Checkout air-quality
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: leeworks-agents/air-quality
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
path: air-quality
|
||||
|
||||
- name: Copy openapi.yaml specs into docs-site
|
||||
run: |
|
||||
mkdir -p api-company/docs-site/public/specs
|
||||
cp zip-enrichment/openapi.yaml api-company/docs-site/public/specs/zip-enrichment.yaml
|
||||
cp holidays/openapi.yaml api-company/docs-site/public/specs/holidays.yaml
|
||||
cp air-quality/openapi.yaml api-company/docs-site/public/specs/air-quality.yaml
|
||||
echo "Specs copied:"
|
||||
ls -la api-company/docs-site/public/specs/
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install docs-site dependencies
|
||||
working-directory: api-company/docs-site
|
||||
run: npm ci
|
||||
|
||||
- name: Build docs-site
|
||||
working-directory: api-company/docs-site
|
||||
run: npm run build
|
||||
|
||||
- name: Log in to container registry
|
||||
run: |
|
||||
echo "${{ secrets.GITEA_TOKEN }}" | docker login registry.leeworks.dev \
|
||||
-u ${{ gitea.actor }} --password-stdin
|
||||
|
||||
- name: Build and push docs-site image
|
||||
working-directory: api-company/docs-site
|
||||
run: |
|
||||
IMAGE="registry.leeworks.dev/leeworks-agents/docs-site"
|
||||
SHA="${{ gitea.sha }}"
|
||||
docker build -t "$IMAGE:$SHA" -t "$IMAGE:latest" .
|
||||
docker push "$IMAGE:$SHA"
|
||||
docker push "$IMAGE:latest"
|
||||
echo "Pushed $IMAGE:$SHA"
|
||||
|
||||
- name: Trigger Flux reconcile (optional)
|
||||
run: |
|
||||
echo "Image pushed. Flux will detect new tag via image automation and re-deploy docs-site."
|
||||
echo "If image automation is not configured, manually run: flux reconcile helmrelease docs-site -n docs-site"
|
||||
@@ -1,61 +0,0 @@
|
||||
# Validate and publish OpenAPI specs to RapidAPI when apis/*/openapi.yaml changes on main.
|
||||
# Requires secrets (configured once RapidAPI listings are live):
|
||||
# RAPIDAPI_PLATFORM_KEY, RAPIDAPI_ZIP_API_ID, RAPIDAPI_ZIP_VERSION_ID,
|
||||
# RAPIDAPI_HOLIDAYS_API_ID, RAPIDAPI_HOLIDAYS_VERSION_ID,
|
||||
# RAPIDAPI_AQI_API_ID, RAPIDAPI_AQI_VERSION_ID,
|
||||
# RAPIDAPI_VIN_API_ID, RAPIDAPI_VIN_VERSION_ID
|
||||
#
|
||||
# If RAPIDAPI_PLATFORM_KEY is not set the publish step exits 0 with a skip message.
|
||||
|
||||
name: Validate and Publish OpenAPI Specs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apis/*/openapi.yaml'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Lint and publish specs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout (with history for diff)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Redocly CLI
|
||||
run: npm install -g @redocly/cli@latest
|
||||
|
||||
- name: Lint OpenAPI specs
|
||||
run: |
|
||||
echo "Linting all OpenAPI specs..."
|
||||
npx @redocly/cli lint apis/*/openapi.yaml
|
||||
|
||||
- name: Detect changed specs
|
||||
id: changed
|
||||
run: |
|
||||
changed=$(git diff --name-only HEAD~1 HEAD | grep 'openapi\.yaml' || true)
|
||||
echo "Changed specs: ${changed:-none}"
|
||||
echo "files=${changed}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish specs to RapidAPI
|
||||
env:
|
||||
RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_PLATFORM_KEY }}
|
||||
RAPIDAPI_ZIP_API_ID: ${{ secrets.RAPIDAPI_ZIP_API_ID }}
|
||||
RAPIDAPI_ZIP_VERSION_ID: ${{ secrets.RAPIDAPI_ZIP_VERSION_ID }}
|
||||
RAPIDAPI_HOLIDAYS_API_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_API_ID }}
|
||||
RAPIDAPI_HOLIDAYS_VERSION_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_VERSION_ID }}
|
||||
RAPIDAPI_AQI_API_ID: ${{ secrets.RAPIDAPI_AQI_API_ID }}
|
||||
RAPIDAPI_AQI_VERSION_ID: ${{ secrets.RAPIDAPI_AQI_VERSION_ID }}
|
||||
RAPIDAPI_VIN_API_ID: ${{ secrets.RAPIDAPI_VIN_API_ID }}
|
||||
RAPIDAPI_VIN_VERSION_ID: ${{ secrets.RAPIDAPI_VIN_VERSION_ID }}
|
||||
run: node scripts/publish-openapi.js
|
||||
@@ -1,21 +0,0 @@
|
||||
# Smoke test: confirms Gitea Act Runner is online and accepting jobs.
|
||||
# Run manually via workflow_dispatch after runner is registered (#77).
|
||||
# Closes leeworks-agents/api-company#96
|
||||
|
||||
name: Runner Smoke Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
name: Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Confirm runner is online
|
||||
run: |
|
||||
echo "Runner is online!"
|
||||
echo "Job ID: $GITHUB_JOB"
|
||||
echo "Runner OS: $(uname -a)"
|
||||
echo "Date: $(date -u)"
|
||||
echo "Smoke test PASSED"
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Validate Flux manifests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
kustomize-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install kustomize
|
||||
run: |
|
||||
curl -sL "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
||||
sudo mv kustomize /usr/local/bin/
|
||||
|
||||
- name: kustomize build flux/
|
||||
run: kustomize build flux/ > /dev/null
|
||||
@@ -15,7 +15,6 @@ Meta-repo for the `leeworks-apis` recursive build loop. This repo holds cross-cu
|
||||
- `MASTER_BUILD_PROMPT.md` — the prime directive driving the build loop
|
||||
- `ROADMAP.md` — cross-repo milestones (Phase 0–6)
|
||||
- `STATUS.md` — living revenue + infra scorecard
|
||||
- `docs/pre-launch-checklist.md` — go-live gate checklist (run the day before public launch)
|
||||
- `SESSION_LOG.md` — append-only log of agent work sessions
|
||||
- `research/RESEARCH_LOG.md` — market research findings, candidate API ideas
|
||||
- `flux/` — cluster-level Flux manifests (registry, runner, monitoring, docs-site)
|
||||
|
||||
+2
-12
@@ -2,8 +2,6 @@
|
||||
|
||||
Cross-repo milestones. Per-API phases live in each API repo's ROADMAP.md.
|
||||
|
||||
> **As of 2026-05-30, a 4th API (VIN Decoder) has been added. Per-API phases also live in `leeworks-agents/vin-decoder`.**
|
||||
|
||||
## Phase 0 — Orientation (one-time)
|
||||
- [ ] Audit cluster: nodes, namespaces, ingress, Flux state → `docs/cluster-audit.md`
|
||||
- [ ] Add Flux GitRepository + Kustomization watching `leeworks-agents/api-company`
|
||||
@@ -12,38 +10,30 @@ Cross-repo milestones. Per-API phases live in each API repo's ROADMAP.md.
|
||||
|
||||
## Phase 1 — API Contracts
|
||||
Each API repo writes its `openapi.yaml` first and lints it. Spec is the source of truth.
|
||||
- See: `leeworks-agents/zip-enrichment`, `holidays`, `air-quality`, `vin-decoder`
|
||||
- See: `leeworks-agents/zip-enrichment`, `holidays`, `air-quality`
|
||||
|
||||
## Phase 2 — Data
|
||||
Per-API: pull free public data sources, cache locally (SQLite or in-memory), schedule monthly re-seed.
|
||||
- VIN Decoder: NHTSA vPIC as data source — SQLite cache seeded on demand (issue #120)
|
||||
|
||||
## Phase 3 — Servers
|
||||
Per-API: implement Fastify/FastAPI service matching `openapi.yaml` exactly. RapidAPI proxy-secret middleware on every route.
|
||||
- VIN Decoder: Fastify server implementation (issue #121)
|
||||
|
||||
## Phase 4 — Monitoring
|
||||
- [ ] Prometheus + Grafana via Flux HelmRelease (`flux/monitoring/`)
|
||||
- [ ] Gatus public status page at `status.leeworks.dev`
|
||||
- [ ] Instrument every API with `api_requests_total`, `api_response_duration_seconds`, `api_data_freshness_seconds`
|
||||
- [ ] VIN Decoder metrics instrumentation + Grafana dashboard (issue #129)
|
||||
- [x] VIN Decoder Gatus health check (issue #134 — closed ✅)
|
||||
|
||||
## Phase 5 — Documentation site
|
||||
- [ ] `docs-site/` Astro + Redoc skeleton
|
||||
- [ ] Gitea Actions pipeline that copies per-API `openapi.yaml` files at build time
|
||||
- [ ] Deploy to `docs.leeworks.dev` via Flux
|
||||
- [ ] One SEO blog post per API minimum (see master prompt §Phase 5)
|
||||
- [x] VIN Decoder docs page (issue #123 — closed ✅)
|
||||
- [x] VIN Decoder SEO blog post (issue #124 — closed ✅)
|
||||
|
||||
## Phase 6 — Pre-launch
|
||||
- [ ] `docs/legal/terms-of-service.md`
|
||||
- [ ] `docs/legal/privacy-policy.md`
|
||||
- [ ] `docs/legal/acceptable-use-policy.md`
|
||||
- [ ] DNS: `zip.leeworks.dev`, `holidays.leeworks.dev`, `aqi.leeworks.dev`, `docs.`, `status.`, `registry.`
|
||||
- [x] VIN Decoder DNS `vin.leeworks.dev` (issue #130 — closed ✅)
|
||||
- [x] VIN Decoder RapidAPI listing (issue #131 — closed ✅ as tracker; operator action required)
|
||||
- [ ] PayPal linked to RapidAPI (manual, on 18th birthday)
|
||||
- [ ] Paid tiers enabled on RapidAPI listings (manual)
|
||||
|
||||
@@ -57,4 +47,4 @@ $100/mo **net** ≈ $136/mo gross after RapidAPI 25% + PayPal ~2%.
|
||||
| 7 Pro @ $19 | $97 net |
|
||||
| 3 Ultra @ $49 | $107 net |
|
||||
|
||||
(Per-API rough math; mix-and-match across all 4 APIs — ZIP Enrichment, Holidays, Air Quality, VIN Decoder.)
|
||||
(Per-API rough math; mix-and-match across the 3 APIs.)
|
||||
|
||||
@@ -1,178 +1,26 @@
|
||||
# Company Status
|
||||
|
||||
_Last updated: 2026-05-31 (sprint planning cycle)_
|
||||
_Last updated: 2026-05-18 (bootstrap)_
|
||||
|
||||
## APIs
|
||||
| API | Spec | Code | Deployed | Listed on RapidAPI | Paying Users | MRR |
|
||||
|----------------|------|------|----------|--------------------|--------------|-----|
|
||||
| ZIP Enrichment | [~] | [~] | [ ] | [ ] | 0 | $0 |
|
||||
| Holidays | [~] | [~] | [ ] | [ ] | 0 | $0 |
|
||||
| ZIP Enrichment | [ ] | [ ] | [ ] | [ ] | 0 | $0 |
|
||||
| Holidays | [ ] | [ ] | [ ] | [ ] | 0 | $0 |
|
||||
| Air Quality | [ ] | [ ] | [ ] | [ ] | 0 | $0 |
|
||||
| VIN Decoder | [x] | [ ] | [ ] | [ ] | 0 | $0 |
|
||||
|
||||
Legend: [x]=done, [~]=in-progress, [ ]=not started
|
||||
|
||||
## Infrastructure
|
||||
- Cluster nodes: 3 control plane (10.0.1.3, .4, .5) + workers (testing1)
|
||||
- **Flux wiring (api-company):** Manifests staged at `flux/api-company-source/` — PENDING Talos merge (issue #2)
|
||||
- **Gitea Actions runner:** Flux manifest committed at `flux/gitea-runner/` — PENDING runner token secret + Talos wiring (issue #3)
|
||||
- **Container registry:** Gitea built-in registry selected; docs/registry.md committed — PENDING Gitea packages enabled (issue #4)
|
||||
- **Prometheus + Grafana:** Flux HelmRelease at `flux/monitoring/` — PENDING Flux wiring + Grafana secret (issue #7)
|
||||
- **Gatus status page:** Flux HelmRelease at `flux/monitoring/gatus-helmrelease.yaml` — PENDING Flux wiring (issue #8)
|
||||
- **API service manifests:** `flux/zip-enrichment/`, `flux/holidays/`, `flux/air-quality/` scaffolded (PR #48, closes #46)
|
||||
- **Image automation:** `flux/image-automation/` — ImageRepository + ImagePolicy + ImageUpdateAutomation for all three APIs
|
||||
- **VIN Decoder manifests:** `flux/vin-decoder/` — Flux HelmRelease + Kustomization committed (issue #140)
|
||||
- Flux healthy: TBD — see Phase-0 issue #1
|
||||
- Gitea Actions runner: DOWN (Phase-0 issue #3)
|
||||
- VPS tunnel: TBD
|
||||
- Container registry: NOT DEPLOYED (Phase-0 issue #4)
|
||||
|
||||
## Completed This Cycle (2026-05-31 — Sprint Cycle)
|
||||
- **#146** — STATUS.md updated to reflect VIN Decoder sprint wave (#117–#141): APIs table row added, completed wave documented, blockers updated.
|
||||
- **#145** — ROADMAP.md updated to reference VIN Decoder as 4th API across all phases (Phase 1–6). `kustomize build flux/` = PASS.
|
||||
- **PR #147 opened** — docs: update ROADMAP.md and STATUS.md for VIN Decoder sprint wave (closes #145, #146).
|
||||
## Revenue
|
||||
- Gross MRR: $0
|
||||
- Net MRR (after ~26.5% fees): $0
|
||||
- Target: $100/mo net
|
||||
- Gap: $100
|
||||
|
||||
## Completed Previous Cycle (2026-05-30 — VIN Decoder Sprint Wave)
|
||||
### VIN Decoder issues #117–#141 — status as of 2026-05-31:
|
||||
- **#117** — VIN Decoder spec + feasibility — **closed ✅** (`apis/vin-decoder/openapi.yaml` merged)
|
||||
- **#120** — VIN Decoder data layer (SQLite/NHTSA vPIC) — **open, in progress** (P1 blocker)
|
||||
- **#121** — VIN Decoder Fastify server — **open, in progress** (P1 blocker, depends on #120)
|
||||
- **#122** — Create `leeworks-agents/vin-decoder` repo — **closed ✅**
|
||||
- **#123** — VIN Decoder docs page — **closed ✅**
|
||||
- **#124** — VIN Decoder SEO blog post — **closed ✅**
|
||||
- **#130** — VIN Decoder DNS `vin.leeworks.dev` — **closed ✅**
|
||||
- **#131** — VIN Decoder RapidAPI listing — **closed ✅** (tracker; operator action required)
|
||||
- **#133** — VIN Decoder pricing page integration — **closed ✅**
|
||||
- **#134** — VIN Decoder Gatus health check — **closed ✅**
|
||||
- **#135** — VIN Decoder image automation — **closed ✅**
|
||||
- **#136** — VIN Decoder ExternalSecret manifest — **closed ✅**
|
||||
- **#138** — Extend publish-openapi pipeline to include VIN Decoder — **closed ✅** (PR #142 merged)
|
||||
- **#139** — Operator: add RAPIDAPI_VIN_API_ID + RAPIDAPI_VIN_VERSION_ID secrets — **open** (operator task, blocked on RapidAPI listing)
|
||||
- **#140** — VIN Decoder Flux manifests — **closed ✅** (PR #143 merged)
|
||||
- **#141** — Pricing page updated for VIN Decoder — **closed ✅** (PR #144 merged)
|
||||
|
||||
## Critical Path (operator must complete in order)
|
||||
1. **Create `0xWheatyz/api-company` repo** (#47) — **highest priority, unblocks everything**
|
||||
2. **Add api-company Flux source to Talos** (#90) — needs Talos PR, reference manifests at `flux/api-company-source/`
|
||||
3. **Create `gitea-leeworks-agents-token` secret** in `flux-system` (#76)
|
||||
4. **Create `gitea-runner-token` secret** in `gitea-runner` (#77)
|
||||
5. **Enable Gitea packages + DNS for `registry.leeworks.dev`** (#4)
|
||||
6. **All other secrets** (#70, #73, #74, #79, #80, #81, #83) follow in order
|
||||
|
||||
## Blockers — VIN Decoder critical path
|
||||
- **P1: #120** — VIN Decoder data layer (NHTSA vPIC SQLite cache) — agent work pending
|
||||
- **P1: #121** — VIN Decoder Fastify server — agent work pending, depends on #120
|
||||
- **P1: #127** — Create `gitea-registry` imagePullSecret in `vin-decoder` namespace — operator manual task
|
||||
- **P2: #128** — Create `rapidapi-proxy-secret` in `vin-decoder` namespace — operator manual task
|
||||
- **P2: #129** — VIN Decoder Prometheus metrics instrumentation — agent work pending, depends on #121
|
||||
- **P3: #139** — Add RAPIDAPI_VIN_* Actions secrets — operator task, depends on RapidAPI listing (#131)
|
||||
|
||||
## New Issues This Sprint (2026-05-30, #120–#146)
|
||||
- **#120** — VIN Decoder data layer — **open, P1**
|
||||
- **#121** — VIN Decoder Fastify server — **open, P1**
|
||||
- **#126** — Operator: GITEA_TOKEN secret in vin-decoder repo — manual operator task
|
||||
- **#127** — Operator: gitea-registry imagePullSecret in vin-decoder namespace — manual operator task, P1 blocker
|
||||
- **#128** — Operator: rapidapi-proxy-secret in vin-decoder namespace — manual operator task
|
||||
- **#129** — VIN Decoder Prometheus metrics — **open, P2, depends on #121**
|
||||
- **#139** — Operator: RAPIDAPI_VIN_* secrets — **open, P3, operator task**
|
||||
- **#145** — Update ROADMAP.md for VIN Decoder — **DONE (this cycle)**
|
||||
- **#146** — Update STATUS.md for VIN Decoder sprint wave — **DONE (this cycle)**
|
||||
|
||||
## Completed Previous Cycle (2026-05-30 — Sprint Cycle)
|
||||
- **#108** — STATUS.md updated with 2026-05-30 sprint plan and critical path.
|
||||
- **#107** — docs-site build verified: `npm run build` exits 0; all 9 required routes present in `dist/`.
|
||||
- **PR #105 MERGED** — `scripts/publish-openapi.js` + `.gitea/workflows/publish-openapi.yaml` (closes #100).
|
||||
- **PR #103 MERGED** — `docs-site/src/pages/pricing.astro` + nav link (closes #101).
|
||||
- **PR #104 MERGED** — `research/RESEARCH_LOG.md` first session: VIN Decoder next candidate (closes #102).
|
||||
- **New issues triaged** — #100–#108 (sprint planning cycle issues reviewed).
|
||||
- **`0xWheatyz/api-company`** — still does not exist; fork sync skipped (blocker #47).
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-28 — Cycle-2 Triage)
|
||||
- **#83** — Triaged: `GITEA_TOKEN` Actions secrets needed in `zip-enrichment`, `holidays`, `air-quality` repos. Manual operator task; reuse token from #74 if it has `write:package` scope. Step-by-step instructions posted.
|
||||
- **No open PRs** — queue empty.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
- **Upstream `0xWheatyz/api-company`** — still does not exist; fork sync skipped (blocker #47).
|
||||
|
||||
## Completed Previous Cycle (2026-05-28 — Late-Night Triage)
|
||||
- **#79** — Triaged: `gitea-registry` imagePullSecret needed in 4 namespaces. Manual operator task; kubectl docker-registry secret instructions posted.
|
||||
- **#80** — Triaged: `gitea-image-automation-token` secret needed in `flux-system`. Manual operator task; write-scoped PAT required, kubectl instructions posted.
|
||||
- **#81** — Triaged: `rapidapi-proxy-secret` needed in `zip-enrichment`, `holidays`, `air-quality`. Manual operator task; placeholder + real-secret commands posted.
|
||||
- **No open PRs** — queue empty.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-28 — Night Triage)
|
||||
- **#76** — Triaged: `gitea-leeworks-agents-token` secret required in `flux-system` to unblock Flux GitRepository auth. Manual operator task; kubectl instructions + reconcile steps posted in issue comment.
|
||||
- **#77** — Triaged: `gitea-runner-token` secret required in `gitea-runner` namespace to register Act Runner. Manual operator task; full step-by-step instructions posted in issue comment.
|
||||
- **No open PRs** — queue empty.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-28 — Evening Triage)
|
||||
- **#74** — Triaged: `GITEA_TOKEN` Actions secret required in leeworks-agents/api-company for docs-site CI. Manual operator task; instructions posted.
|
||||
- **#73** — Triaged: `gatus-slack-webhook` secret required in `monitoring` namespace. Manual operator task; kubectl instructions posted.
|
||||
- **#67** — Confirmed already resolved: `docs/operator-runbook.md` and `docs/dns.md` both list all 7 subdomains incl. `grafana.leeworks.dev` (PR #63). No code change needed.
|
||||
- **No open PRs** — queue empty.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-28 — Morning Triage)
|
||||
- **#69 → PR #71 MERGED** — Added `.gitea/workflows/validate-flux.yaml` CI workflow: runs `kustomize build flux/` on every PR and push to `main`. CI gate enforced once Act Runner (#3) is online.
|
||||
- **#70** — Triaged: `grafana-admin` secret needed in `monitoring` namespace. Manual operator task; kubectl instructions posted in issue comment.
|
||||
- **#67** — Confirmed `docs/operator-runbook.md` Phase 4 DNS table already lists all 7 subdomains including grafana.leeworks.dev. No code change needed.
|
||||
- **#66** — Blocked on Phase 0 operator actions (#47, #2, #3, #4). Status comment posted.
|
||||
- **#47, #2, #3, #4, #7, #8, #16, #17, #18, #19, #27, #30, #33, #44** — Status comments posted confirming blocked/awaiting-operator state.
|
||||
- **No open PRs to review** — queue empty after PR #71 merged.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-27 — Evening Triage)
|
||||
- **Triage pass** — all 15 open agent-ready issues reviewed; status comments posted on each.
|
||||
- **No open PRs** — nothing to merge or review.
|
||||
- **No new feature work** — all issues remain blocked on operator actions or cross-repo prerequisites.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-27 — Ship/Afternoon)
|
||||
- **PRs reviewed** — 0 open PRs on fork; nothing to merge.
|
||||
- **Upstream sync** — `0xWheatyz/api-company` does not yet exist (blocker #41/#47); `git fetch upstream` failed as expected. Fork `origin/main` is current (SHA `9a1639f`).
|
||||
- **`kustomize build flux/` = PASS** — all sub-trees validate cleanly; no regressions.
|
||||
- **Deployment PR** — skipped; upstream repo must be created by operator first (#41, #47).
|
||||
- **No new feature work** — all 15 open agent-ready issues remain blocked on operator actions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-27 — Morning)
|
||||
- **Triage pass** — all 15 open agent-ready issues reviewed; status comments posted/confirmed on each.
|
||||
- **#19** — First triage comment posted: RapidAPI/PayPal operator task fully documented; all agent-side prerequisites (OpenAPI specs, listing copy, proxy-secret placeholders) confirmed ready.
|
||||
- **No PRs to review** — fork had no open PRs.
|
||||
- **No new feature work** — all open issues blocked on operator actions.
|
||||
|
||||
## Completed Previous Cycle (2026-05-26 Evening)
|
||||
- **#50** — `docs/operator-runbook.md` added: ordered phase-by-phase manual for operator. PR #52 merged.
|
||||
- **#51** — Flux image automation: `ImageRepository` + `ImagePolicy` + `ImageUpdateAutomation` for all three API services; setter markers added to HelmReleases. PR #52 merged. `kustomize build flux/ = PASS`.
|
||||
|
||||
## Completed Previous Cycle (2026-05-26 PM)
|
||||
- **#46** — Scaffolded Flux deployment manifests for all three API services (zip-enrichment, holidays, air-quality). PR #48 merged.
|
||||
|
||||
## Completed Previous Cycle (2026-05-26 AM)
|
||||
- **#43** — secrets-checklist.md added (closed, PR #45)
|
||||
- **#36** — Cluster audit committed to `docs/cluster-audit.md` (closed)
|
||||
- **#40** — Legal docs (ToS, Privacy Policy, AUP) under `docs/legal/` (closed)
|
||||
- **#37** — docs-site Astro skeleton with Redoc pages; `npm run build` passes (closed)
|
||||
- **#39** — SEO blog posts (ZIP, Holidays, Air Quality) in `docs-site/src/pages/blog/` (closed)
|
||||
- **#38** — Gitea Actions CI workflow (`.gitea/workflows/build-docs.yaml`) + Dockerfile (closed)
|
||||
- **#34** — Cluster audit PR merged
|
||||
|
||||
## Flux Manifests (kustomize build flux/ = PASS)
|
||||
All flux manifests validate successfully. Committed components pending Flux activation:
|
||||
- `gitea-runner` namespace + HelmRelease (gitea-act-runner chart)
|
||||
- `monitoring` namespace + kube-prometheus-stack HelmRelease
|
||||
- `monitoring` Gatus HelmRelease (status.leeworks.dev, 90-day retention)
|
||||
- `docs-site` HelmRelease (docs.leeworks.dev)
|
||||
- `zip-enrichment` namespace + HelmRelease (zip.leeworks.dev) + rapidapi-proxy-secret placeholder
|
||||
- `holidays` namespace + HelmRelease (holidays.leeworks.dev) + rapidapi-proxy-secret placeholder
|
||||
- `air-quality` namespace + HelmRelease (aqi.leeworks.dev) + rapidapi-proxy-secret placeholder
|
||||
- `vin-decoder` namespace + HelmRelease (vin.leeworks.dev) + Flux manifests
|
||||
- `image-automation` ImageRepository + ImagePolicy + ImageUpdateAutomation for all three APIs
|
||||
|
||||
## Blockers (human operator action required)
|
||||
1. **Create `0xWheatyz/api-company` repo on Gitea** — every ship cycle fails until this exists (#41, #47) **<-- DO THIS FIRST**
|
||||
2. **Add api-company GitRepository+Kustomization to 0xWheatyz/Talos** at `testing1/first-cluster/cluster/flux/` — reference manifests ready in `flux/api-company-source/` (#2)
|
||||
3. **Create `gitea-leeworks-agents-token` secret** in `flux-system` namespace (HTTPS token for Gitea)
|
||||
4. **Create `gitea-runner-token` secret** in `gitea-runner` namespace (Gitea Admin -> Actions -> Runners -> New Runner) (#3)
|
||||
5. **Enable Gitea packages** (for container registry at `registry.leeworks.dev`) (#4)
|
||||
6. **Create `gitea-registry` imagePullSecret in `vin-decoder` namespace** (#127) — VIN Decoder P1
|
||||
7. **RapidAPI + PayPal setup** — manual, gated on operator turning 18 (#19, #44)
|
||||
|
||||
> Full ordered runbook with copy-paste commands: `docs/operator-runbook.md`
|
||||
## Next action
|
||||
Phase-0 cluster audit (issue #1 in this repo). Until Flux is confirmed watching this org and the runner+registry are up, no API CI can land.
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: VIN Decoder API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Decode any 17-character Vehicle Identification Number (VIN) into structured
|
||||
vehicle data including make, model, year, trim, engine, body style, and more.
|
||||
Powered by the NHTSA vPIC public-domain database.
|
||||
|
||||
**Data source:** NHTSA Product Information Catalog and Vehicle Listing (vPIC)
|
||||
public API - US federal government data, public domain (17 U.S.C. 105).
|
||||
|
||||
**Coverage:** Model years 1981-present, all major manufacturers registered
|
||||
with NHTSA (domestic and import).
|
||||
|
||||
**Caching:** Decoded results are cached for 90 days; cache status is
|
||||
indicated by the X-Cache response header.
|
||||
contact:
|
||||
name: leeworks.dev API Support
|
||||
url: https://docs.leeworks.dev
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
|
||||
servers:
|
||||
- url: https://vin.leeworks.dev/v1
|
||||
description: Production
|
||||
|
||||
security:
|
||||
- RapidApiProxy: []
|
||||
|
||||
tags:
|
||||
- name: decode
|
||||
description: VIN decoding endpoints
|
||||
- name: health
|
||||
description: Service health and observability
|
||||
|
||||
paths:
|
||||
/decode:
|
||||
get:
|
||||
operationId: decodeVin
|
||||
summary: Decode a single VIN
|
||||
description: |
|
||||
Decodes a 17-character VIN and returns structured vehicle attributes.
|
||||
Results are cached for 90 days; a cache hit is indicated by
|
||||
X-Cache: HIT in the response headers.
|
||||
tags:
|
||||
- decode
|
||||
parameters:
|
||||
- name: vin
|
||||
in: query
|
||||
required: true
|
||||
description: 17-character Vehicle Identification Number (uppercase, no I/O/Q).
|
||||
schema:
|
||||
type: string
|
||||
minLength: 17
|
||||
maxLength: 17
|
||||
pattern: "^[A-HJ-NPR-Z0-9]{17}$"
|
||||
example: 1HGCM82633A004352
|
||||
- name: raw
|
||||
in: query
|
||||
required: false
|
||||
description: If true, include raw NHTSA vPIC fields in the response.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
"200":
|
||||
description: VIN successfully decoded
|
||||
headers:
|
||||
X-Cache:
|
||||
schema:
|
||||
type: string
|
||||
enum: [HIT, MISS]
|
||||
description: Whether the result was served from cache
|
||||
X-Data-Source:
|
||||
schema:
|
||||
type: string
|
||||
description: Upstream data source identifier
|
||||
X-Request-Id:
|
||||
schema:
|
||||
type: string
|
||||
description: Unique request identifier
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VinDecodeResult"
|
||||
"400":
|
||||
description: Invalid VIN format or missing parameter
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
description: Missing or invalid RapidAPI proxy secret
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"429":
|
||||
description: Rate limit exceeded for your plan
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
description: Internal server error or upstream NHTSA API failure
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/batch:
|
||||
post:
|
||||
operationId: decodeVinBatch
|
||||
summary: Decode up to 50 VINs in a single request
|
||||
description: |
|
||||
Accepts a JSON body with an array of VINs (1-50) and returns a decoded
|
||||
result for each. Each VIN is processed independently; partial failures
|
||||
return an error object in that position rather than failing the whole batch.
|
||||
tags:
|
||||
- decode
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- vins
|
||||
properties:
|
||||
vins:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 50
|
||||
items:
|
||||
type: string
|
||||
minLength: 17
|
||||
maxLength: 17
|
||||
pattern: "^[A-HJ-NPR-Z0-9]{17}$"
|
||||
description: Array of 17-character VINs to decode
|
||||
example:
|
||||
vins:
|
||||
- 1HGCM82633A004352
|
||||
- WBABW33486PX01612
|
||||
responses:
|
||||
"200":
|
||||
description: Batch decode results (one entry per input VIN, in order)
|
||||
headers:
|
||||
X-Request-Id:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/VinDecodeResult"
|
||||
- $ref: "#/components/schemas/VinDecodeError"
|
||||
count:
|
||||
type: integer
|
||||
description: Total number of VINs processed
|
||||
cached_count:
|
||||
type: integer
|
||||
description: Number of results served from cache
|
||||
error_count:
|
||||
type: integer
|
||||
description: Number of VINs that could not be decoded
|
||||
"400":
|
||||
description: Invalid request body
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
description: Missing or invalid RapidAPI proxy secret
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"429":
|
||||
description: Rate limit exceeded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/health:
|
||||
get:
|
||||
operationId: healthCheck
|
||||
summary: Service health check
|
||||
description: |
|
||||
Returns health status of the VIN Decoder service including cache
|
||||
statistics and NHTSA API reachability. Does not require X-RapidAPI-Proxy-Secret.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Service is healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
"503":
|
||||
description: Service is degraded (upstream unreachable or DB error)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
RapidApiProxy:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-RapidAPI-Proxy-Secret
|
||||
description: |
|
||||
RapidAPI proxy secret injected automatically by RapidAPI on every
|
||||
subscriber request. Direct callers must include this header manually.
|
||||
|
||||
schemas:
|
||||
VinDecodeResult:
|
||||
type: object
|
||||
required:
|
||||
- vin
|
||||
- error_code
|
||||
properties:
|
||||
vin:
|
||||
type: string
|
||||
description: The input VIN (uppercased)
|
||||
example: 1HGCM82633A004352
|
||||
make:
|
||||
type: ["string", "null"]
|
||||
description: Vehicle manufacturer brand
|
||||
example: HONDA
|
||||
model:
|
||||
type: ["string", "null"]
|
||||
description: Vehicle model name
|
||||
example: Accord
|
||||
model_year:
|
||||
type: ["string", "null"]
|
||||
description: Model year as a 4-digit string
|
||||
example: "2003"
|
||||
trim:
|
||||
type: ["string", "null"]
|
||||
description: Trim level (e.g. EX, LX, Sport)
|
||||
example: EX
|
||||
series:
|
||||
type: ["string", "null"]
|
||||
description: Series designation if applicable
|
||||
body_class:
|
||||
type: ["string", "null"]
|
||||
description: Body style classification
|
||||
example: Sedan/Saloon
|
||||
drive_type:
|
||||
type: ["string", "null"]
|
||||
description: Drive configuration
|
||||
example: FWD/Front-Wheel Drive
|
||||
engine_displacement_cc:
|
||||
type: ["number", "null"]
|
||||
description: Engine displacement in cubic centimetres
|
||||
example: 2354
|
||||
engine_displacement_l:
|
||||
type: ["number", "null"]
|
||||
description: Engine displacement in litres
|
||||
example: 2.4
|
||||
engine_cylinders:
|
||||
type: ["integer", "null"]
|
||||
description: Number of engine cylinders
|
||||
example: 4
|
||||
fuel_type_primary:
|
||||
type: ["string", "null"]
|
||||
description: Primary fuel type
|
||||
example: Gasoline
|
||||
transmission_style:
|
||||
type: ["string", "null"]
|
||||
description: Transmission type (Automatic, Manual, CVT, etc.)
|
||||
example: Automatic
|
||||
transmission_speeds:
|
||||
type: ["string", "null"]
|
||||
description: Number of transmission speeds as string
|
||||
example: "5"
|
||||
plant_city:
|
||||
type: ["string", "null"]
|
||||
description: Assembly plant city
|
||||
example: MARYSVILLE
|
||||
plant_state:
|
||||
type: ["string", "null"]
|
||||
description: Assembly plant state/province
|
||||
example: OHIO
|
||||
plant_country:
|
||||
type: ["string", "null"]
|
||||
description: Assembly plant country
|
||||
example: UNITED STATES (USA)
|
||||
manufacturer_name:
|
||||
type: ["string", "null"]
|
||||
description: Full legal name of the manufacturer
|
||||
example: HONDA OF AMERICA MFG., INC.
|
||||
vehicle_type:
|
||||
type: ["string", "null"]
|
||||
description: NHTSA vehicle type classification
|
||||
example: PASSENGER CAR
|
||||
error_code:
|
||||
type: string
|
||||
description: NHTSA decode error code. "0" means successful decode.
|
||||
example: "0"
|
||||
error_text:
|
||||
type: ["string", "null"]
|
||||
description: Human-readable decode error (null when error_code is "0")
|
||||
cached:
|
||||
type: boolean
|
||||
description: Whether this result was served from the local cache
|
||||
example: true
|
||||
|
||||
VinDecodeError:
|
||||
type: object
|
||||
required:
|
||||
- vin
|
||||
- error
|
||||
- message
|
||||
properties:
|
||||
vin:
|
||||
type: string
|
||||
description: The VIN that could not be decoded
|
||||
error:
|
||||
type: string
|
||||
description: Error code
|
||||
example: INVALID_VIN
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
example: VIN must be exactly 17 alphanumeric characters
|
||||
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
- message
|
||||
- status
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Machine-readable error code
|
||||
example: BAD_REQUEST
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
example: "Query parameter 'vin' is required"
|
||||
status:
|
||||
type: integer
|
||||
description: HTTP status code
|
||||
example: 400
|
||||
|
||||
HealthResponse:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- version
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ok, degraded]
|
||||
description: Overall service health
|
||||
version:
|
||||
type: string
|
||||
description: Service version
|
||||
example: "1.0.0"
|
||||
uptime_seconds:
|
||||
type: integer
|
||||
description: Seconds since the service started
|
||||
example: 86400
|
||||
cache:
|
||||
type: object
|
||||
properties:
|
||||
entries:
|
||||
type: integer
|
||||
description: Number of cached VIN records
|
||||
hit_rate_24h:
|
||||
type: number
|
||||
description: Cache hit rate over the last 24 hours (0.0-1.0)
|
||||
size_mb:
|
||||
type: number
|
||||
description: SQLite cache file size in megabytes
|
||||
upstream:
|
||||
type: object
|
||||
properties:
|
||||
nhtsa_vpic:
|
||||
type: string
|
||||
enum: [reachable, unreachable]
|
||||
description: NHTSA vPIC API reachability
|
||||
last_check:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO-8601 timestamp of last upstream health check
|
||||
@@ -0,0 +1 @@
|
||||
# placeholder — populated by Phase-4/5 issues
|
||||
@@ -1,14 +0,0 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Serve with nginx
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,9 +0,0 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://docs.leeworks.dev',
|
||||
integrations: [mdx(), sitemap()],
|
||||
output: 'static',
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|svg|ico|woff2?)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Health check
|
||||
location /health {
|
||||
return 200 "ok\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "leeworks-docs-site",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.8.0",
|
||||
"@astrojs/mdx": "^3.0.0",
|
||||
"@astrojs/sitemap": "^3.1.0",
|
||||
"redoc": "^2.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
}
|
||||
@@ -1,405 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: VIN Decoder API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Decode any 17-character Vehicle Identification Number (VIN) into structured
|
||||
vehicle data including make, model, year, trim, engine, body style, and more.
|
||||
Powered by the NHTSA vPIC public-domain database.
|
||||
|
||||
**Data source:** NHTSA Product Information Catalog and Vehicle Listing (vPIC)
|
||||
public API - US federal government data, public domain (17 U.S.C. 105).
|
||||
|
||||
**Coverage:** Model years 1981-present, all major manufacturers registered
|
||||
with NHTSA (domestic and import).
|
||||
|
||||
**Caching:** Decoded results are cached for 90 days; cache status is
|
||||
indicated by the X-Cache response header.
|
||||
contact:
|
||||
name: leeworks.dev API Support
|
||||
url: https://docs.leeworks.dev
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
|
||||
servers:
|
||||
- url: https://vin.leeworks.dev/v1
|
||||
description: Production
|
||||
|
||||
security:
|
||||
- RapidApiProxy: []
|
||||
|
||||
tags:
|
||||
- name: decode
|
||||
description: VIN decoding endpoints
|
||||
- name: health
|
||||
description: Service health and observability
|
||||
|
||||
paths:
|
||||
/decode:
|
||||
get:
|
||||
operationId: decodeVin
|
||||
summary: Decode a single VIN
|
||||
description: |
|
||||
Decodes a 17-character VIN and returns structured vehicle attributes.
|
||||
Results are cached for 90 days; a cache hit is indicated by
|
||||
X-Cache: HIT in the response headers.
|
||||
tags:
|
||||
- decode
|
||||
parameters:
|
||||
- name: vin
|
||||
in: query
|
||||
required: true
|
||||
description: 17-character Vehicle Identification Number (uppercase, no I/O/Q).
|
||||
schema:
|
||||
type: string
|
||||
minLength: 17
|
||||
maxLength: 17
|
||||
pattern: "^[A-HJ-NPR-Z0-9]{17}$"
|
||||
example: 1HGCM82633A004352
|
||||
- name: raw
|
||||
in: query
|
||||
required: false
|
||||
description: If true, include raw NHTSA vPIC fields in the response.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
"200":
|
||||
description: VIN successfully decoded
|
||||
headers:
|
||||
X-Cache:
|
||||
schema:
|
||||
type: string
|
||||
enum: [HIT, MISS]
|
||||
description: Whether the result was served from cache
|
||||
X-Data-Source:
|
||||
schema:
|
||||
type: string
|
||||
description: Upstream data source identifier
|
||||
X-Request-Id:
|
||||
schema:
|
||||
type: string
|
||||
description: Unique request identifier
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VinDecodeResult"
|
||||
"400":
|
||||
description: Invalid VIN format or missing parameter
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
description: Missing or invalid RapidAPI proxy secret
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"429":
|
||||
description: Rate limit exceeded for your plan
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
description: Internal server error or upstream NHTSA API failure
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/batch:
|
||||
post:
|
||||
operationId: decodeVinBatch
|
||||
summary: Decode up to 50 VINs in a single request
|
||||
description: |
|
||||
Accepts a JSON body with an array of VINs (1-50) and returns a decoded
|
||||
result for each. Each VIN is processed independently; partial failures
|
||||
return an error object in that position rather than failing the whole batch.
|
||||
tags:
|
||||
- decode
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- vins
|
||||
properties:
|
||||
vins:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 50
|
||||
items:
|
||||
type: string
|
||||
minLength: 17
|
||||
maxLength: 17
|
||||
pattern: "^[A-HJ-NPR-Z0-9]{17}$"
|
||||
description: Array of 17-character VINs to decode
|
||||
example:
|
||||
vins:
|
||||
- 1HGCM82633A004352
|
||||
- WBABW33486PX01612
|
||||
responses:
|
||||
"200":
|
||||
description: Batch decode results (one entry per input VIN, in order)
|
||||
headers:
|
||||
X-Request-Id:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/VinDecodeResult"
|
||||
- $ref: "#/components/schemas/VinDecodeError"
|
||||
count:
|
||||
type: integer
|
||||
description: Total number of VINs processed
|
||||
cached_count:
|
||||
type: integer
|
||||
description: Number of results served from cache
|
||||
error_count:
|
||||
type: integer
|
||||
description: Number of VINs that could not be decoded
|
||||
"400":
|
||||
description: Invalid request body
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
description: Missing or invalid RapidAPI proxy secret
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"429":
|
||||
description: Rate limit exceeded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/health:
|
||||
get:
|
||||
operationId: healthCheck
|
||||
summary: Service health check
|
||||
description: |
|
||||
Returns health status of the VIN Decoder service including cache
|
||||
statistics and NHTSA API reachability. Does not require X-RapidAPI-Proxy-Secret.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Service is healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
"503":
|
||||
description: Service is degraded (upstream unreachable or DB error)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
RapidApiProxy:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-RapidAPI-Proxy-Secret
|
||||
description: |
|
||||
RapidAPI proxy secret injected automatically by RapidAPI on every
|
||||
subscriber request. Direct callers must include this header manually.
|
||||
|
||||
schemas:
|
||||
VinDecodeResult:
|
||||
type: object
|
||||
required:
|
||||
- vin
|
||||
- error_code
|
||||
properties:
|
||||
vin:
|
||||
type: string
|
||||
description: The input VIN (uppercased)
|
||||
example: 1HGCM82633A004352
|
||||
make:
|
||||
type: ["string", "null"]
|
||||
description: Vehicle manufacturer brand
|
||||
example: HONDA
|
||||
model:
|
||||
type: ["string", "null"]
|
||||
description: Vehicle model name
|
||||
example: Accord
|
||||
model_year:
|
||||
type: ["string", "null"]
|
||||
description: Model year as a 4-digit string
|
||||
example: "2003"
|
||||
trim:
|
||||
type: ["string", "null"]
|
||||
description: Trim level (e.g. EX, LX, Sport)
|
||||
example: EX
|
||||
series:
|
||||
type: ["string", "null"]
|
||||
description: Series designation if applicable
|
||||
body_class:
|
||||
type: ["string", "null"]
|
||||
description: Body style classification
|
||||
example: Sedan/Saloon
|
||||
drive_type:
|
||||
type: ["string", "null"]
|
||||
description: Drive configuration
|
||||
example: FWD/Front-Wheel Drive
|
||||
engine_displacement_cc:
|
||||
type: ["number", "null"]
|
||||
description: Engine displacement in cubic centimetres
|
||||
example: 2354
|
||||
engine_displacement_l:
|
||||
type: ["number", "null"]
|
||||
description: Engine displacement in litres
|
||||
example: 2.4
|
||||
engine_cylinders:
|
||||
type: ["integer", "null"]
|
||||
description: Number of engine cylinders
|
||||
example: 4
|
||||
fuel_type_primary:
|
||||
type: ["string", "null"]
|
||||
description: Primary fuel type
|
||||
example: Gasoline
|
||||
transmission_style:
|
||||
type: ["string", "null"]
|
||||
description: Transmission type (Automatic, Manual, CVT, etc.)
|
||||
example: Automatic
|
||||
transmission_speeds:
|
||||
type: ["string", "null"]
|
||||
description: Number of transmission speeds as string
|
||||
example: "5"
|
||||
plant_city:
|
||||
type: ["string", "null"]
|
||||
description: Assembly plant city
|
||||
example: MARYSVILLE
|
||||
plant_state:
|
||||
type: ["string", "null"]
|
||||
description: Assembly plant state/province
|
||||
example: OHIO
|
||||
plant_country:
|
||||
type: ["string", "null"]
|
||||
description: Assembly plant country
|
||||
example: UNITED STATES (USA)
|
||||
manufacturer_name:
|
||||
type: ["string", "null"]
|
||||
description: Full legal name of the manufacturer
|
||||
example: HONDA OF AMERICA MFG., INC.
|
||||
vehicle_type:
|
||||
type: ["string", "null"]
|
||||
description: NHTSA vehicle type classification
|
||||
example: PASSENGER CAR
|
||||
error_code:
|
||||
type: string
|
||||
description: NHTSA decode error code. "0" means successful decode.
|
||||
example: "0"
|
||||
error_text:
|
||||
type: ["string", "null"]
|
||||
description: Human-readable decode error (null when error_code is "0")
|
||||
cached:
|
||||
type: boolean
|
||||
description: Whether this result was served from the local cache
|
||||
example: true
|
||||
|
||||
VinDecodeError:
|
||||
type: object
|
||||
required:
|
||||
- vin
|
||||
- error
|
||||
- message
|
||||
properties:
|
||||
vin:
|
||||
type: string
|
||||
description: The VIN that could not be decoded
|
||||
error:
|
||||
type: string
|
||||
description: Error code
|
||||
example: INVALID_VIN
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
example: VIN must be exactly 17 alphanumeric characters
|
||||
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
- message
|
||||
- status
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Machine-readable error code
|
||||
example: BAD_REQUEST
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
example: "Query parameter 'vin' is required"
|
||||
status:
|
||||
type: integer
|
||||
description: HTTP status code
|
||||
example: 400
|
||||
|
||||
HealthResponse:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- version
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ok, degraded]
|
||||
description: Overall service health
|
||||
version:
|
||||
type: string
|
||||
description: Service version
|
||||
example: "1.0.0"
|
||||
uptime_seconds:
|
||||
type: integer
|
||||
description: Seconds since the service started
|
||||
example: 86400
|
||||
cache:
|
||||
type: object
|
||||
properties:
|
||||
entries:
|
||||
type: integer
|
||||
description: Number of cached VIN records
|
||||
hit_rate_24h:
|
||||
type: number
|
||||
description: Cache hit rate over the last 24 hours (0.0-1.0)
|
||||
size_mb:
|
||||
type: number
|
||||
description: SQLite cache file size in megabytes
|
||||
upstream:
|
||||
type: object
|
||||
properties:
|
||||
nhtsa_vpic:
|
||||
type: string
|
||||
enum: [reachable, unreachable]
|
||||
description: NHTSA vPIC API reachability
|
||||
last_check:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO-8601 timestamp of last upstream health check
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
const { title, description = "leeworks.dev API documentation" } = Astro.props;
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title} | leeworks.dev APIs</title>
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, -apple-system, sans-serif; background: #0f1117; color: #e2e8f0; }
|
||||
nav { background: #1a1d27; border-bottom: 1px solid #2d3748; padding: 0 2rem; display: flex; align-items: center; gap: 2rem; height: 60px; }
|
||||
nav a { color: #90cdf4; text-decoration: none; font-weight: 500; }
|
||||
nav a:hover { color: #fff; }
|
||||
nav .brand { font-size: 1.25rem; font-weight: 700; color: #fff; }
|
||||
main { min-height: calc(100vh - 120px); }
|
||||
footer { background: #1a1d27; border-top: 1px solid #2d3748; padding: 1.5rem 2rem; text-align: center; color: #718096; font-size: 0.875rem; }
|
||||
footer a { color: #90cdf4; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" class="brand">leeworks.dev</a>
|
||||
<a href="/zip-enrichment">ZIP Enrichment</a>
|
||||
<a href="/holidays">Holidays</a>
|
||||
<a href="/air-quality">Air Quality</a>
|
||||
<a href="/pricing">Pricing</a>
|
||||
<a href="/blog">Blog</a>
|
||||
<a href="https://rapidapi.com/leeworks" target="_blank" rel="noopener">RapidAPI</a>
|
||||
</nav>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<footer>
|
||||
<p>
|
||||
© 2026 leeworks.dev —
|
||||
<a href="/legal/terms-of-service">Terms</a> ·
|
||||
<a href="/legal/privacy-policy">Privacy</a> ·
|
||||
<a href="/legal/acceptable-use-policy">AUP</a> ·
|
||||
<a href="https://status.leeworks.dev" target="_blank" rel="noopener">Status</a>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
|
||||
const apiName = 'air-quality';
|
||||
const titles: Record<string, string> = {
|
||||
'zip-enrichment': 'ZIP Enrichment API',
|
||||
'holidays': 'Holidays API',
|
||||
'air-quality': 'Air Quality API',
|
||||
};
|
||||
const title = titles[apiName];
|
||||
---
|
||||
<Base title={title} description={`${title} — OpenAPI documentation`}>
|
||||
<style>
|
||||
#redoc-container { background: #fff; }
|
||||
</style>
|
||||
<div id="redoc-container"></div>
|
||||
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
|
||||
// Load Redoc from CDN
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
|
||||
script.onload = function () {
|
||||
Redoc.init(specUrl, {
|
||||
theme: {
|
||||
colors: { primary: { main: '#667eea' } },
|
||||
typography: { fontFamily: 'system-ui, sans-serif' },
|
||||
},
|
||||
}, document.getElementById('redoc-container'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,178 +0,0 @@
|
||||
---
|
||||
title: "Air Quality API: Real-Time AQI Data for Any Location"
|
||||
description: "Access real-time Air Quality Index (AQI) data, PM2.5, PM10, and health recommendations for any city worldwide using the leeworks.dev Air Quality API."
|
||||
date: "2026-05-24"
|
||||
author: "leeworks.dev"
|
||||
tags: ["air-quality", "aqi", "api", "tutorial"]
|
||||
---
|
||||
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
<Base title="Air Quality API Guide" description="Access real-time AQI data for any location worldwide.">
|
||||
|
||||
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "Air Quality API: Real-Time AQI Data for Any Location",
|
||||
"datePublished": "2026-05-24",
|
||||
"author": { "@type": "Organization", "name": "leeworks.dev" },
|
||||
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
|
||||
})} />
|
||||
|
||||
# Air Quality API: Real-Time AQI Data for Any Location
|
||||
|
||||
Whether you're building a fitness app, a travel planner, or a smart home dashboard, air quality data is increasingly essential. The leeworks.dev **Air Quality API** gives you real-time AQI readings, pollutant breakdowns, and health recommendations for any location in the world.
|
||||
|
||||
## What Is AQI and Why Does Your App Need It?
|
||||
|
||||
The **Air Quality Index (AQI)** is a standardized scale (0–500) that communicates how clean or polluted the air is:
|
||||
|
||||
| AQI | Category | Health Implication |
|
||||
|-----|----------|-------------------|
|
||||
| 0–50 | Good | Air quality is satisfactory |
|
||||
| 51–100 | Moderate | Acceptable for most people |
|
||||
| 101–150 | Unhealthy for Sensitive Groups | At-risk groups may experience effects |
|
||||
| 151–200 | Unhealthy | Everyone may begin to experience health effects |
|
||||
| 201–300 | Very Unhealthy | Health alert: serious effects possible |
|
||||
| 301–500 | Hazardous | Emergency conditions |
|
||||
|
||||
**Use cases for an AQI data API:**
|
||||
|
||||
- **Fitness apps** — warn runners when outdoor exercise is unsafe
|
||||
- **Travel apps** — show air quality forecasts for destination cities
|
||||
- **Real estate platforms** — display neighborhood air quality scores
|
||||
- **Smart home apps** — trigger air purifiers based on outdoor AQI
|
||||
- **Health tracking apps** — correlate symptoms with air quality data
|
||||
- **News and weather apps** — add AQI to daily weather cards
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Get current AQI for a city
|
||||
curl "https://aqi.leeworks.dev/v1/current?city=Los+Angeles&country=US" \
|
||||
-H "X-RapidAPI-Key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"location": {
|
||||
"city": "Los Angeles",
|
||||
"country": "US",
|
||||
"latitude": 34.0522,
|
||||
"longitude": -118.2437
|
||||
},
|
||||
"aqi": 87,
|
||||
"category": "Moderate",
|
||||
"pollutants": {
|
||||
"pm25": 22.4,
|
||||
"pm10": 35.1,
|
||||
"o3": 41.2,
|
||||
"no2": 18.5,
|
||||
"so2": 2.1,
|
||||
"co": 0.4
|
||||
},
|
||||
"health_recommendation": "Unusually sensitive people should consider reducing prolonged outdoor exertion.",
|
||||
"updated_at": "2026-05-24T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## By Coordinates (Lat/Long)
|
||||
|
||||
```bash
|
||||
curl "https://aqi.leeworks.dev/v1/current?lat=48.8566&lon=2.3522" \
|
||||
-H "X-RapidAPI-Key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
async function getAirQuality(city, country = 'US') {
|
||||
const response = await fetch(
|
||||
`https://aqi.leeworks.dev/v1/current?city=${encodeURIComponent(city)}&country=${country}`,
|
||||
{ headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY } }
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const data = await getAirQuality('Denver');
|
||||
if (data.aqi > 100) {
|
||||
console.warn(`Air quality in ${data.location.city} is ${data.category}. Consider staying indoors.`);
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
def get_aqi(lat: float, lon: float) -> dict:
|
||||
resp = httpx.get(
|
||||
"https://aqi.leeworks.dev/v1/current",
|
||||
params={"lat": lat, "lon": lon},
|
||||
headers={"X-RapidAPI-Key": "YOUR_KEY"},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# Example: check AQI before recommending outdoor run
|
||||
aqi_data = get_aqi(37.7749, -122.4194) # San Francisco
|
||||
if aqi_data["aqi"] <= 100:
|
||||
print("Good to go for a run!")
|
||||
else:
|
||||
print(f"Air quality is {aqi_data['category']} — consider indoor exercise.")
|
||||
```
|
||||
|
||||
### React Hook
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface AQIData {
|
||||
aqi: number;
|
||||
category: string;
|
||||
health_recommendation: string;
|
||||
}
|
||||
|
||||
export function useAirQuality(city: string) {
|
||||
const [data, setData] = useState<AQIData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/aqi?city=${encodeURIComponent(city)}`)
|
||||
.then(r => r.json())
|
||||
.then(setData)
|
||||
.finally(() => setLoading(false));
|
||||
}, [city]);
|
||||
|
||||
return { data, loading };
|
||||
}
|
||||
```
|
||||
|
||||
## Data Source
|
||||
|
||||
The leeworks.dev Air Quality API aggregates data from the **OpenAQ** public dataset — a non-profit platform that collects open air quality data from government agencies worldwide. Data is refreshed hourly.
|
||||
|
||||
## Pricing
|
||||
|
||||
| Plan | Requests/mo | Price |
|
||||
|------|------------|-------|
|
||||
| Free | 500 | $0 |
|
||||
| Basic | 10,000 | $9/mo |
|
||||
| Pro | 100,000 | $19/mo |
|
||||
| Ultra | 1,000,000 | $49/mo |
|
||||
|
||||
**[Subscribe on RapidAPI →](https://rapidapi.com/leeworks/api/air-quality)**
|
||||
|
||||
## Conclusion
|
||||
|
||||
Air quality is no longer a niche data point — it's a critical health metric that millions of people check daily. The leeworks.dev **AQI data API** gives your app real-time air quality readings, pollutant breakdowns, and actionable health recommendations with a simple REST interface.
|
||||
|
||||
**[Start building for free →](https://rapidapi.com/leeworks/api/air-quality)**
|
||||
|
||||
</article>
|
||||
</Base>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
const posts = await Astro.glob('./*.mdx');
|
||||
posts.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime());
|
||||
---
|
||||
<Base title="Blog" description="leeworks.dev developer blog">
|
||||
<style>
|
||||
.blog-hero { padding: 3rem 2rem 1rem; text-align: center; }
|
||||
.blog-hero h1 { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; }
|
||||
.blog-hero p { color: #a0aec0; }
|
||||
.posts { max-width: 800px; margin: 2rem auto; padding: 0 2rem; }
|
||||
.post-card { border-bottom: 1px solid #2d3748; padding: 2rem 0; }
|
||||
.post-card h2 a { color: #90cdf4; text-decoration: none; font-size: 1.5rem; }
|
||||
.meta { color: #718096; font-size: 0.875rem; margin: 0.5rem 0; }
|
||||
.description { color: #a0aec0; }
|
||||
</style>
|
||||
<div class="blog-hero"><h1>Blog</h1><p>Tutorials and news from leeworks.dev</p></div>
|
||||
<div class="posts">
|
||||
{posts.map(post => (
|
||||
<div class="post-card">
|
||||
<h2><a href={post.url}>{post.frontmatter.title}</a></h2>
|
||||
<div class="meta">{post.frontmatter.date}</div>
|
||||
<p class="description">{post.frontmatter.description}</p>
|
||||
</div>
|
||||
))}
|
||||
{posts.length === 0 && <p style="color: #718096">No posts yet.</p>}
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,161 +0,0 @@
|
||||
---
|
||||
title: "Public Holidays API: The Free Holiday Calendar API for Any Country"
|
||||
description: "Get public holidays for 100+ countries with a single API call. The leeworks.dev Holidays API is perfect for scheduling, calendar apps, and payroll systems."
|
||||
date: "2026-05-24"
|
||||
author: "leeworks.dev"
|
||||
tags: ["holidays", "api", "tutorial"]
|
||||
---
|
||||
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
<Base title="Public Holidays API Guide" description="Get public holidays for 100+ countries with a single API call.">
|
||||
|
||||
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "Public Holidays API: The Free Holiday Calendar API for Any Country",
|
||||
"datePublished": "2026-05-24",
|
||||
"author": { "@type": "Organization", "name": "leeworks.dev" },
|
||||
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
|
||||
})} />
|
||||
|
||||
# Public Holidays API: The Free Holiday Calendar API for Any Country
|
||||
|
||||
Building a scheduling app, payroll system, or booking platform? You need accurate **public holiday data** for every country you serve. The leeworks.dev **Holidays API** gives you that data in milliseconds.
|
||||
|
||||
## Why You Need a Holiday Calendar API
|
||||
|
||||
Manually maintaining a list of public holidays is a losing battle. Holidays change year to year, differ by country and region, and missing one can mean:
|
||||
|
||||
- **Wrong delivery estimates** on e-commerce sites
|
||||
- **Incorrect payroll calculations** (overtime on holidays)
|
||||
- **Broken calendar apps** that schedule meetings on national holidays
|
||||
- **Failed SLA commitments** that assumed business days
|
||||
|
||||
A reliable **holiday API** solves this once.
|
||||
|
||||
## What the leeworks.dev Holidays API Provides
|
||||
|
||||
- Public holidays for **100+ countries**
|
||||
- Data updated from Nager.Date's curated public dataset
|
||||
- Filter by **country code** (ISO 3166-1 alpha-2), **year**, and **type**
|
||||
- Response includes holiday name (localized), date, and type (`public`, `optional`, `observance`)
|
||||
- Sub-100ms response time, SQLite-backed
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Get all US public holidays for 2026
|
||||
curl "https://holidays.leeworks.dev/v1/holidays?country=US&year=2026" \
|
||||
-H "X-RapidAPI-Key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"country": "US",
|
||||
"year": 2026,
|
||||
"holidays": [
|
||||
{
|
||||
"date": "2026-01-01",
|
||||
"name": "New Year's Day",
|
||||
"type": "public"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-04",
|
||||
"name": "Independence Day",
|
||||
"type": "public"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Skip Holidays in Business Day Calculations
|
||||
|
||||
```python
|
||||
from datetime import date, timedelta
|
||||
import httpx
|
||||
|
||||
def next_business_day(start: date, country: str = "US") -> date:
|
||||
resp = httpx.get(
|
||||
"https://holidays.leeworks.dev/v1/holidays",
|
||||
params={"country": country, "year": start.year},
|
||||
headers={"X-RapidAPI-Key": "YOUR_KEY"},
|
||||
)
|
||||
holidays = {h["date"] for h in resp.json()["holidays"]}
|
||||
|
||||
current = start + timedelta(days=1)
|
||||
while current.weekday() >= 5 or current.isoformat() in holidays:
|
||||
current += timedelta(days=1)
|
||||
return current
|
||||
```
|
||||
|
||||
### 2. Display Holiday Badges in a Calendar
|
||||
|
||||
```javascript
|
||||
async function getHolidayMap(countryCode, year) {
|
||||
const res = await fetch(
|
||||
`https://holidays.leeworks.dev/v1/holidays?country=${countryCode}&year=${year}`,
|
||||
{ headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY } }
|
||||
);
|
||||
const { holidays } = await res.json();
|
||||
// Return a Map of ISO date string → holiday name
|
||||
return new Map(holidays.map(h => [h.date, h.name]));
|
||||
}
|
||||
|
||||
// Usage in a calendar component
|
||||
const holidayMap = await getHolidayMap('GB', 2026);
|
||||
const isHoliday = holidayMap.has('2026-12-25'); // true: Christmas Day
|
||||
```
|
||||
|
||||
### 3. Check If Today Is a Holiday
|
||||
|
||||
```typescript
|
||||
async function isTodayHoliday(country = 'US'): Promise<string | null> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const res = await fetch(
|
||||
`https://holidays.leeworks.dev/v1/is-holiday?country=${country}&date=${today}`,
|
||||
{ headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY! } }
|
||||
);
|
||||
const data = await res.json();
|
||||
return data.isHoliday ? data.name : null;
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Countries (Sample)
|
||||
|
||||
| Code | Country | Code | Country |
|
||||
|------|---------|------|---------|
|
||||
| US | United States | GB | United Kingdom |
|
||||
| CA | Canada | DE | Germany |
|
||||
| FR | France | JP | Japan |
|
||||
| AU | Australia | BR | Brazil |
|
||||
| IN | India | MX | Mexico |
|
||||
|
||||
...and 90+ more. Use `GET /v1/countries` to see the full list.
|
||||
|
||||
## Pricing
|
||||
|
||||
| Plan | Requests/mo | Price |
|
||||
|------|------------|-------|
|
||||
| Free | 500 | $0 |
|
||||
| Basic | 10,000 | $9/mo |
|
||||
| Pro | 100,000 | $19/mo |
|
||||
| Ultra | 1,000,000 | $49/mo |
|
||||
|
||||
**[Subscribe on RapidAPI →](https://rapidapi.com/leeworks/api/holidays)**
|
||||
|
||||
## Conclusion
|
||||
|
||||
Stop hardcoding holiday lists or scraping Wikipedia. The leeworks.dev **public holidays API** gives you accurate, up-to-date holiday data for every country you need — with a simple REST interface and affordable pricing.
|
||||
|
||||
**[Get started for free →](https://rapidapi.com/leeworks/api/holidays)**
|
||||
|
||||
</article>
|
||||
</Base>
|
||||
@@ -1,340 +0,0 @@
|
||||
---
|
||||
title: "How to Decode a VIN Number with Node.js Using Free NHTSA Data"
|
||||
description: "Learn how to decode any 17-character Vehicle Identification Number (VIN) with Node.js using the free NHTSA vPIC database — or skip the plumbing and call the leeworks.dev VIN Decoder API directly."
|
||||
date: "2026-05-30"
|
||||
author: "leeworks.dev"
|
||||
tags: ["vin-decoder", "nodejs", "automotive", "api", "tutorial"]
|
||||
---
|
||||
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
<Base title="How to Decode a VIN Number with Node.js Using Free NHTSA Data" description="Learn how to decode any 17-character Vehicle Identification Number (VIN) with Node.js using the free NHTSA vPIC database — or skip the plumbing and call the leeworks.dev VIN Decoder API directly.">
|
||||
|
||||
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "How to Decode a VIN Number with Node.js Using Free NHTSA Data",
|
||||
"datePublished": "2026-05-30",
|
||||
"author": { "@type": "Organization", "name": "leeworks.dev" },
|
||||
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
|
||||
})} />
|
||||
|
||||
# How to Decode a VIN Number with Node.js Using Free NHTSA Data
|
||||
|
||||
Every vehicle sold in the United States since 1981 carries a unique 17-character fingerprint stamped into the chassis: the **Vehicle Identification Number**, or VIN. Decode it and you unlock make, model, year, trim level, engine type, body class, transmission, plant of manufacture, and more — without paying Carfax $40 per report.
|
||||
|
||||
In this tutorial you'll learn how VINs are structured, how to query the free NHTSA vPIC database directly in Node.js, and how to call the **leeworks.dev VIN Decoder API** for a production-ready solution that handles caching, error handling, and batch decoding out of the box.
|
||||
|
||||
---
|
||||
|
||||
## What Is a VIN?
|
||||
|
||||
A VIN is a 17-character alphanumeric string divided into three logical sections:
|
||||
|
||||
| Section | Characters | Name | What It Encodes |
|
||||
|---------|-----------|------|-----------------|
|
||||
| **WMI** | 1–3 | World Manufacturer Identifier | Country of origin + manufacturer |
|
||||
| **VDS** | 4–9 | Vehicle Descriptor Section | Model, body style, engine type, check digit |
|
||||
| **VIS** | 10–17 | Vehicle Identifier Section | Model year, plant, sequential serial number |
|
||||
|
||||
### Breaking down a real VIN
|
||||
|
||||
Take `1HGCM82633A004352` — a 2003 Honda Accord EX:
|
||||
|
||||
- `1HG` → Manufactured in the USA by Honda
|
||||
- `CM826` → Accord EX 4-door sedan, 2.4L i-VTEC engine (position 9 = check digit `3`)
|
||||
- `3` → Model year 2003 (position 10)
|
||||
- `A` → Marysville, Ohio assembly plant (position 11)
|
||||
- `004352` → Sequential production number
|
||||
|
||||
VIN characters deliberately exclude `I`, `O`, and `Q` to avoid confusion with `1`, `0`, and `0` respectively — something to remember when validating user input.
|
||||
|
||||
---
|
||||
|
||||
## Why NHTSA vPIC?
|
||||
|
||||
The **NHTSA Product Information Catalog and Vehicle Listing (vPIC)** is a US federal government database maintained by the National Highway Traffic Safety Administration. It covers:
|
||||
|
||||
- All model years 1981 to present
|
||||
- Every manufacturer registered with NHTSA (domestic and imported)
|
||||
- 70+ decoded attributes per VIN including engine displacement, fuel type, GVWR, and more
|
||||
- **No API key, no rate limits** (beyond fair-use throttling), **public domain** under 17 U.S.C. 105
|
||||
|
||||
The base endpoint is:
|
||||
|
||||
```
|
||||
https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/{vin}?format=json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Calling NHTSA vPIC Directly in Node.js
|
||||
|
||||
Here's a minimal Node.js script using the built-in `fetch` API (Node 18+):
|
||||
|
||||
```js
|
||||
// decode-vin.js
|
||||
const VIN = process.argv[2] ?? '1HGCM82633A004352';
|
||||
|
||||
async function decodeVin(vin) {
|
||||
// Validate: 17 chars, no I/O/Q
|
||||
if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(vin)) {
|
||||
throw new Error(`Invalid VIN format: ${vin}`);
|
||||
}
|
||||
|
||||
const url = `https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/${vin}?format=json`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`NHTSA returned HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const r = json.Results[0];
|
||||
|
||||
return {
|
||||
vin: r.VIN,
|
||||
make: r.Make,
|
||||
model: r.Model,
|
||||
modelYear: r.ModelYear,
|
||||
trim: r.Trim,
|
||||
series: r.Series,
|
||||
bodyClass: r.BodyClass,
|
||||
driveType: r.DriveType,
|
||||
engineDisplacementL: r.DisplacementL,
|
||||
engineCylinders: r.EngineCylinders,
|
||||
fuelTypePrimary: r.FuelTypePrimary,
|
||||
transmissionStyle: r.TransmissionStyle,
|
||||
manufacturerName: r.Manufacturer,
|
||||
plantCity: r.PlantCity,
|
||||
plantState: r.PlantState,
|
||||
plantCountry: r.PlantCountry,
|
||||
errorCode: r.ErrorCode,
|
||||
errorText: r.ErrorText,
|
||||
};
|
||||
}
|
||||
|
||||
decodeVin(VIN)
|
||||
.then(data => console.log(JSON.stringify(data, null, 2)))
|
||||
.catch(err => { console.error(err.message); process.exit(1); });
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
node decode-vin.js 1HGCM82633A004352
|
||||
```
|
||||
|
||||
Expected output (abridged):
|
||||
|
||||
```json
|
||||
{
|
||||
"vin": "1HGCM82633A004352",
|
||||
"make": "HONDA",
|
||||
"model": "Accord",
|
||||
"modelYear": "2003",
|
||||
"trim": "EX",
|
||||
"bodyClass": "Sedan/Saloon",
|
||||
"driveType": "FWD/Front-Wheel Drive",
|
||||
"engineDisplacementL": "2.4",
|
||||
"engineCylinders": "4",
|
||||
"fuelTypePrimary": "Gasoline",
|
||||
"transmissionStyle": "Automatic",
|
||||
"manufacturerName": "HONDA OF AMERICA MFG., INC.",
|
||||
"plantCity": "MARYSVILLE",
|
||||
"plantState": "OHIO",
|
||||
"plantCountry": "UNITED STATES (USA)"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Problem with Rolling Your Own
|
||||
|
||||
Calling NHTSA directly works great for a quick script. But for a production application, you'll quickly run into friction:
|
||||
|
||||
1. **No caching** — every request hits the NHTSA servers. At scale, this is slow (NHTSA p99 ≈ 800ms) and risks being throttled.
|
||||
2. **Raw NHTSA response** — the flat key/value array has 80+ fields, many empty; you need to map and filter these yourself.
|
||||
3. **No batch support** — decoding 50 VINs means 50 sequential round-trips.
|
||||
4. **No SLA** — the NHTSA API is a government service; it has no uptime guarantee.
|
||||
5. **Header boilerplate** — proxy-secret validation, request IDs, CORS headers — you write it every time.
|
||||
|
||||
---
|
||||
|
||||
## Using the leeworks.dev VIN Decoder API
|
||||
|
||||
The **leeworks.dev VIN Decoder API** wraps NHTSA vPIC with a 90-day SQLite cache, pre-mapped response schema, and batch endpoint — all available on RapidAPI.
|
||||
|
||||
### Single VIN decode
|
||||
|
||||
```js
|
||||
// Using the leeworks.dev VIN Decoder API
|
||||
const VIN = '1HGCM82633A004352';
|
||||
const API_KEY = process.env.RAPIDAPI_KEY; // Your RapidAPI key
|
||||
|
||||
const res = await fetch(`https://vin.leeworks.dev/v1/decode?vin=${VIN}`, {
|
||||
headers: {
|
||||
'X-RapidAPI-Key': API_KEY,
|
||||
'X-RapidAPI-Host': 'vin.leeworks.dev',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
console.log(`${data.make} ${data.model} (${data.model_year})`);
|
||||
// → HONDA Accord (2003)
|
||||
|
||||
// Check cache status
|
||||
const cacheStatus = res.headers.get('X-Cache'); // "HIT" or "MISS"
|
||||
console.log(`Cache: ${cacheStatus}`);
|
||||
```
|
||||
|
||||
### Batch decode (up to 50 VINs)
|
||||
|
||||
```js
|
||||
const vins = [
|
||||
'1HGCM82633A004352', // 2003 Honda Accord
|
||||
'1FTFW1ET5DFA18803', // 2013 Ford F-150
|
||||
'WBA3A5G59DNP26082', // 2013 BMW 3 Series
|
||||
];
|
||||
|
||||
const res = await fetch('https://vin.leeworks.dev/v1/batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-RapidAPI-Key': API_KEY,
|
||||
'X-RapidAPI-Host': 'vin.leeworks.dev',
|
||||
},
|
||||
body: JSON.stringify({ vins }),
|
||||
});
|
||||
|
||||
const { results, count, cached_count } = await res.json();
|
||||
console.log(`Decoded ${count} VINs, ${cached_count} from cache`);
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.error) {
|
||||
console.log(`${r.vin}: ERROR — ${r.error}`);
|
||||
} else {
|
||||
console.log(`${r.vin}: ${r.make} ${r.model} ${r.model_year}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Health check
|
||||
|
||||
```js
|
||||
// No auth required on /health
|
||||
const health = await fetch('https://vin.leeworks.dev/v1/health').then(r => r.json());
|
||||
console.log(`Status: ${health.status}, Cache: ${health.cache.total_entries} entries`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-World Use Cases
|
||||
|
||||
### Automotive apps and dealership software
|
||||
|
||||
Show instant vehicle details when a user types a VIN at checkout or trade-in. Cache the result — the same VIN is often looked up dozens of times across different users.
|
||||
|
||||
```js
|
||||
async function enrichListing(listingVin) {
|
||||
const vehicle = await decodeVinCached(listingVin);
|
||||
return {
|
||||
title: `${vehicle.model_year} ${vehicle.make} ${vehicle.model} ${vehicle.trim}`,
|
||||
engine: `${vehicle.engine_displacement_l}L ${vehicle.engine_cylinders}-cyl ${vehicle.fuel_type_primary}`,
|
||||
drivetrain: vehicle.drive_type,
|
||||
body: vehicle.body_class,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Insurance tech and underwriting
|
||||
|
||||
Premium calculators, claims systems, and underwriting platforms need reliable vehicle specs. A VIN decode call returns body class (sedan vs. SUV vs. pickup) and engine details in under 50ms with a cache hit — fast enough for real-time quote generation.
|
||||
|
||||
### Fleet management platforms
|
||||
|
||||
Decode entire fleets in a single batch call. The `/v1/batch` endpoint processes up to 50 VINs per request, making it practical to seed a database of 10,000 fleet vehicles with 200 API calls rather than 10,000 sequential hits.
|
||||
|
||||
### Used car marketplaces
|
||||
|
||||
User-generated listings often contain VIN typos or incorrect specs. Validate and auto-fill vehicle details server-side on listing creation:
|
||||
|
||||
```js
|
||||
app.post('/listings', async (req, res) => {
|
||||
const { vin, ...listing } = req.body;
|
||||
|
||||
// Validate + enrich
|
||||
const vehicle = await vinApi.decode(vin);
|
||||
if (vehicle.error_code !== '0') {
|
||||
return res.status(422).json({ error: 'Invalid or unrecognised VIN' });
|
||||
}
|
||||
|
||||
const enriched = { ...listing, vin, make: vehicle.make, model: vehicle.model, year: vehicle.model_year };
|
||||
await db.listings.create(enriched);
|
||||
res.status(201).json(enriched);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VIN Validation
|
||||
|
||||
Before calling any API, validate the VIN client-side to save an unnecessary round-trip:
|
||||
|
||||
```js
|
||||
function isValidVin(vin) {
|
||||
// 17 chars, alphanumeric excluding I, O, Q
|
||||
if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(vin)) return false;
|
||||
|
||||
// Optional: verify check digit (position 9)
|
||||
const weights = [8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2];
|
||||
const transliteration = { A:1,B:2,C:3,D:4,E:5,F:6,G:7,H:8,
|
||||
J:1,K:2,L:3,M:4,N:5,P:7,R:9,S:2,T:3,U:4,V:5,W:6,X:7,Y:8,Z:9 };
|
||||
|
||||
const vals = vin.toUpperCase().split('').map(c =>
|
||||
/\d/.test(c) ? parseInt(c) : transliteration[c]
|
||||
);
|
||||
|
||||
const sum = vals.reduce((acc, v, i) => acc + v * weights[i], 0);
|
||||
const check = sum % 11;
|
||||
const expected = check === 10 ? 'X' : String(check);
|
||||
|
||||
return vin[8].toUpperCase() === expected;
|
||||
}
|
||||
|
||||
console.log(isValidVin('1HGCM82633A004352')); // true
|
||||
console.log(isValidVin('1HGCM82633A00435X')); // false (bad check digit)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## About the Data Source
|
||||
|
||||
The NHTSA vPIC database is maintained by the US Department of Transportation under its statutory mandate (49 U.S.C. § 30111). Manufacturers are legally required to register VIN patterns with NHTSA, so coverage is comprehensive for vehicles sold in the US market.
|
||||
|
||||
Key facts:
|
||||
- **Coverage**: Model years 1981–present; 1980 and earlier VINs were not standardised and are not covered
|
||||
- **Accuracy**: Authoritative for the original vehicle specification; does not reflect modifications, title brands, or recall status
|
||||
- **Update frequency**: NHTSA updates the database when new model variants are registered, typically months before vehicles reach dealerships
|
||||
- **Licence**: US federal government work, public domain under 17 U.S.C. 105 — free to use commercially with no attribution requirement
|
||||
|
||||
---
|
||||
|
||||
## Get Started
|
||||
|
||||
The leeworks.dev VIN Decoder API is available on RapidAPI with a free tier (100 requests/month, no credit card required):
|
||||
|
||||
👉 **[VIN Decoder API on RapidAPI](https://rapidapi.com/leeworks/api/vin-decoder)**
|
||||
|
||||
Full API reference, including request/response schemas and error codes:
|
||||
|
||||
👉 **[API Documentation](/vin-decoder)**
|
||||
|
||||
---
|
||||
|
||||
*Built with ❤️ by [leeworks.dev](https://docs.leeworks.dev) — production-ready data APIs powered by free public-domain data sources.*
|
||||
|
||||
</article>
|
||||
</Base>
|
||||
@@ -1,158 +0,0 @@
|
||||
---
|
||||
title: "ZIP Code Enrichment API: Add Location Intelligence to Your App in Minutes"
|
||||
description: "Learn how to use the leeworks.dev ZIP Code Enrichment API to add city, state, timezone, and demographic data to any postal code lookup."
|
||||
date: "2026-05-24"
|
||||
author: "leeworks.dev"
|
||||
tags: ["zip-enrichment", "api", "tutorial"]
|
||||
---
|
||||
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
<Base title="ZIP Code Enrichment API Guide" description="Learn how to use the leeworks.dev ZIP Code Enrichment API to add city, state, timezone, and demographic data to any postal code lookup.">
|
||||
|
||||
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "ZIP Code Enrichment API: Add Location Intelligence to Your App in Minutes",
|
||||
"datePublished": "2026-05-24",
|
||||
"author": { "@type": "Organization", "name": "leeworks.dev" },
|
||||
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
|
||||
})} />
|
||||
|
||||
# ZIP Code Enrichment API: Add Location Intelligence to Your App in Minutes
|
||||
|
||||
Every time a user types their ZIP code, there's a wealth of data waiting to be unlocked — city name, state, county, timezone, latitude, longitude, and more. The **leeworks.dev ZIP Code Enrichment API** makes it trivially easy to retrieve all of that in a single API call.
|
||||
|
||||
## What Is a ZIP Code Enrichment API?
|
||||
|
||||
A **postal code demographics API** (or ZIP enrichment API) takes a 5-digit US ZIP code as input and returns structured data about that location. This is useful for:
|
||||
|
||||
- **E-commerce** — display the user's city/state after they type a ZIP, skip the state dropdown
|
||||
- **Shipping calculators** — determine timezone and region for delivery estimates
|
||||
- **Analytics dashboards** — group customers by region, state, or county
|
||||
- **Lead scoring** — enrich CRM contacts with location data automatically
|
||||
- **Form UX** — auto-fill city/state fields for a smoother checkout experience
|
||||
|
||||
## Why Build on leeworks.dev?
|
||||
|
||||
Unlike scraping Google Maps or paying for expensive enterprise solutions, the leeworks.dev ZIP Enrichment API:
|
||||
|
||||
- Returns **sub-50ms responses** (SQLite-backed, no external dependencies)
|
||||
- Provides **100% US ZIP code coverage** using the free USPS/Census dataset
|
||||
- Is available on **RapidAPI** with a generous free tier
|
||||
- Has a **simple, well-documented REST API** following the OpenAPI 3.1 standard
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Get Your API Key
|
||||
|
||||
Sign up on [RapidAPI](https://rapidapi.com/leeworks/api/zip-enrichment) and subscribe to a plan. The Free tier gives you 500 requests/month.
|
||||
|
||||
### 2. Make Your First Call
|
||||
|
||||
```bash
|
||||
curl -X GET "https://zip.leeworks.dev/v1/lookup?zip=90210" \
|
||||
-H "X-RapidAPI-Key: YOUR_API_KEY" \
|
||||
-H "X-RapidAPI-Host: zip.leeworks.dev"
|
||||
```
|
||||
|
||||
### 3. Parse the Response
|
||||
|
||||
```json
|
||||
{
|
||||
"zip": "90210",
|
||||
"city": "Beverly Hills",
|
||||
"state": "CA",
|
||||
"state_full": "California",
|
||||
"county": "Los Angeles",
|
||||
"timezone": "America/Los_Angeles",
|
||||
"latitude": 34.0901,
|
||||
"longitude": -118.4065,
|
||||
"population": 20124
|
||||
}
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### JavaScript / Node.js
|
||||
|
||||
```javascript
|
||||
const response = await fetch('https://zip.leeworks.dev/v1/lookup?zip=10001', {
|
||||
headers: {
|
||||
'X-RapidAPI-Key': process.env.RAPIDAPI_KEY,
|
||||
'X-RapidAPI-Host': 'zip.leeworks.dev',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(`${data.city}, ${data.state} (${data.timezone})`);
|
||||
// → "New York, NY (America/New_York)"
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
resp = httpx.get(
|
||||
"https://zip.leeworks.dev/v1/lookup",
|
||||
params={"zip": "60601"},
|
||||
headers={
|
||||
"X-RapidAPI-Key": "YOUR_API_KEY",
|
||||
"X-RapidAPI-Host": "zip.leeworks.dev",
|
||||
},
|
||||
)
|
||||
data = resp.json()
|
||||
print(f"{data['city']}, {data['state']}")
|
||||
# → "Chicago, IL"
|
||||
```
|
||||
|
||||
## Pricing
|
||||
|
||||
| Plan | Requests/mo | Price | Best for |
|
||||
|------|------------|-------|---------|
|
||||
| Free | 500 | $0 | Prototyping |
|
||||
| Basic | 10,000 | $9/mo | Small apps |
|
||||
| Pro | 100,000 | $19/mo | Growing products |
|
||||
| Ultra | 1,000,000 | $49/mo | High volume |
|
||||
|
||||
**[Subscribe on RapidAPI →](https://rapidapi.com/leeworks/api/zip-enrichment)**
|
||||
|
||||
## Use Case: Auto-fill City/State on Checkout
|
||||
|
||||
Here's a complete React component that auto-fills city and state when a user enters their ZIP:
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
export function ZipField() {
|
||||
const [zip, setZip] = useState('');
|
||||
const [location, setLocation] = useState<{ city: string; state: string } | null>(null);
|
||||
|
||||
const handleZipChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, '').slice(0, 5);
|
||||
setZip(value);
|
||||
if (value.length === 5) {
|
||||
const res = await fetch(`/api/zip-lookup?zip=${value}`);
|
||||
if (res.ok) setLocation(await res.json());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={zip} onChange={handleZipChange} placeholder="ZIP Code" maxLength={5} />
|
||||
{location && <p>📍 {location.city}, {location.state}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The leeworks.dev **ZIP code enrichment API** is the fastest way to add location intelligence to any application. With a simple GET request, you get city, state, county, timezone, and coordinates — no geocoding, no rate-limit headaches.
|
||||
|
||||
**[Get started for free →](https://rapidapi.com/leeworks/api/zip-enrichment)**
|
||||
|
||||
</article>
|
||||
</Base>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
|
||||
const apiName = 'holidays';
|
||||
const titles: Record<string, string> = {
|
||||
'zip-enrichment': 'ZIP Enrichment API',
|
||||
'holidays': 'Holidays API',
|
||||
'air-quality': 'Air Quality API',
|
||||
};
|
||||
const title = titles[apiName];
|
||||
---
|
||||
<Base title={title} description={`${title} — OpenAPI documentation`}>
|
||||
<style>
|
||||
#redoc-container { background: #fff; }
|
||||
</style>
|
||||
<div id="redoc-container"></div>
|
||||
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
|
||||
// Load Redoc from CDN
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
|
||||
script.onload = function () {
|
||||
Redoc.init(specUrl, {
|
||||
theme: {
|
||||
colors: { primary: { main: '#667eea' } },
|
||||
typography: { fontFamily: 'system-ui, sans-serif' },
|
||||
},
|
||||
}, document.getElementById('redoc-container'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="Home" description="leeworks.dev — production-ready data APIs: ZIP Enrichment, Holidays, Air Quality">
|
||||
<style>
|
||||
.hero { padding: 5rem 2rem 3rem; text-align: center; }
|
||||
.hero h1 { font-size: 3rem; font-weight: 800; background: linear-gradient(135deg, #90cdf4, #667eea); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 1rem; }
|
||||
.hero p { font-size: 1.25rem; color: #a0aec0; max-width: 600px; margin: 0 auto 2rem; }
|
||||
.cta { display: inline-block; background: #667eea; color: #fff; padding: 0.75rem 2rem; border-radius: 8px; text-decoration: none; font-weight: 600; }
|
||||
.apis { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1100px; margin: 0 auto; }
|
||||
.api-card { background: #1a1d27; border: 1px solid #2d3748; border-radius: 12px; padding: 1.5rem; }
|
||||
.api-card h2 { color: #90cdf4; margin-bottom: 0.5rem; }
|
||||
.api-card p { color: #a0aec0; margin-bottom: 1rem; font-size: 0.95rem; }
|
||||
.badge { display: inline-block; font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 4px; margin-bottom: 0.75rem; }
|
||||
.badge.wip { background: #744210; color: #fbd38d; }
|
||||
.badge.live { background: #1a4731; color: #9ae6b4; }
|
||||
.links a { color: #90cdf4; text-decoration: none; margin-right: 1rem; }
|
||||
.links a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Simple. Reliable. APIs.</h1>
|
||||
<p>Production-ready data APIs for ZIP enrichment, public holidays, air quality, and VIN decoding. Available on RapidAPI.</p>
|
||||
<a href="https://rapidapi.com/leeworks" class="cta" target="_blank" rel="noopener">Get API Key on RapidAPI</a>
|
||||
</div>
|
||||
|
||||
<div class="apis">
|
||||
<div class="api-card">
|
||||
<span class="badge wip">In Development</span>
|
||||
<h2>ZIP Enrichment API</h2>
|
||||
<p>Enrich US ZIP codes with city, state, county, timezone, lat/long, and population data.</p>
|
||||
<div class="links">
|
||||
<a href="/zip-enrichment">Docs</a>
|
||||
<a href="https://rapidapi.com/leeworks/api/zip-enrichment" target="_blank" rel="noopener">RapidAPI</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<span class="badge wip">In Development</span>
|
||||
<h2>Holidays API</h2>
|
||||
<p>Public holidays for 100+ countries, filterable by country, year, and type.</p>
|
||||
<div class="links">
|
||||
<a href="/holidays">Docs</a>
|
||||
<a href="https://rapidapi.com/leeworks/api/holidays" target="_blank" rel="noopener">RapidAPI</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<span class="badge wip">In Development</span>
|
||||
<h2>Air Quality API</h2>
|
||||
<p>Real-time and historical AQI data worldwide including PM2.5, PM10, and health recommendations.</p>
|
||||
<div class="links">
|
||||
<a href="/air-quality">Docs</a>
|
||||
<a href="https://rapidapi.com/leeworks/api/air-quality" target="_blank" rel="noopener">RapidAPI</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<span class="badge wip">In Development</span>
|
||||
<h2>VIN Decoder API</h2>
|
||||
<p>Decode any 17-character VIN into make, model, year, trim, engine, body class, and more. Powered by the NHTSA vPIC public-domain database.</p>
|
||||
<div class="links">
|
||||
<a href="/vin-decoder">Docs</a>
|
||||
<a href="https://rapidapi.com/leeworks/api/vin-decoder" target="_blank" rel="noopener">RapidAPI</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="text-align: center; padding: 2rem; border-top: 1px solid #2d3748; margin-top: 3rem; color: #718096; font-size: 0.875rem;">
|
||||
<p style="margin-bottom: 0.5rem;">© 2026 leeworks.dev — All rights reserved.</p>
|
||||
<nav style="display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap;">
|
||||
<a href="/legal/terms-of-service" style="color: #a0aec0; text-decoration: none;">Terms of Service</a>
|
||||
<a href="/legal/privacy-policy" style="color: #a0aec0; text-decoration: none;">Privacy Policy</a>
|
||||
<a href="/legal/acceptable-use-policy" style="color: #a0aec0; text-decoration: none;">Acceptable Use Policy</a>
|
||||
<a href="mailto:legal@leeworks.dev" style="color: #a0aec0; text-decoration: none;">Contact</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</Base>
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
layout: ../../layouts/Base.astro
|
||||
title: Acceptable Use Policy
|
||||
description: leeworks.dev API Acceptable Use Policy
|
||||
---
|
||||
|
||||
# Acceptable Use Policy
|
||||
|
||||
**Effective Date:** 2026-05-24
|
||||
**Contact:** legal@leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This Acceptable Use Policy ("AUP") defines the rules for using leeworks.dev APIs. It applies to all users regardless of plan. Violations may result in immediate account suspension.
|
||||
|
||||
## 2. Rate Limits and Abuse
|
||||
|
||||
### 2.1 Respect Your Plan Limits
|
||||
|
||||
Each subscription plan includes defined rate limits:
|
||||
|
||||
| Plan | Requests/min | Requests/month |
|
||||
|------|-------------|---------------|
|
||||
| Free | 10 | 500 |
|
||||
| Basic | 60 | 10,000 |
|
||||
| Pro | 300 | 100,000 |
|
||||
| Ultra | 1,000 | 1,000,000 |
|
||||
|
||||
### 2.2 Prohibited Rate Limit Circumvention
|
||||
|
||||
The following are explicitly prohibited:
|
||||
- Using multiple API keys or accounts to aggregate quota
|
||||
- Caching responses for redistribution beyond your own application
|
||||
- Rotating IP addresses or using proxies to bypass rate limits
|
||||
|
||||
## 3. Prohibited Uses
|
||||
|
||||
You may **not**:
|
||||
- Download or cache the entire dataset backing any API
|
||||
- Resell, sublicense, or redistribute API access to third parties
|
||||
- Build a competing API product that serves our data to others
|
||||
- Use the APIs for any illegal purpose
|
||||
- Attempt to compromise the security or integrity of our systems
|
||||
- Perform denial-of-service attacks against our infrastructure
|
||||
- Probe our systems for vulnerabilities without prior written authorization
|
||||
|
||||
## 4. Acceptable Uses
|
||||
|
||||
The following are examples of acceptable use:
|
||||
- Integrating ZIP code, holiday, or air quality data into your own product
|
||||
- Building dashboards, mobile apps, or internal tools
|
||||
- Academic research (within Free plan limits)
|
||||
- Automated data fetching within your plan's rate limits
|
||||
|
||||
## 5. Enforcement
|
||||
|
||||
We continuously monitor API usage for abuse. Flagged accounts may be throttled, suspended, or permanently terminated for serious violations.
|
||||
|
||||
## 6. Reporting Abuse
|
||||
|
||||
Report misuse to **legal@leeworks.dev** immediately.
|
||||
|
||||
## 7. Contact
|
||||
|
||||
Questions about this policy: **legal@leeworks.dev**
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
layout: ../../layouts/Base.astro
|
||||
title: Privacy Policy
|
||||
description: leeworks.dev API Privacy Policy
|
||||
---
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
**Effective Date:** 2026-05-24
|
||||
**Contact:** legal@leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
leeworks.dev ("we", "us") operates the ZIP Enrichment, Holidays, and Air Quality APIs. This Privacy Policy describes what data we collect when you use our Services, how we use it, and your rights regarding that data.
|
||||
|
||||
## 2. What Data We Collect
|
||||
|
||||
### 2.1 Request Logs
|
||||
|
||||
When you make API calls, we log:
|
||||
- API key identifier (hashed/truncated — not the full key)
|
||||
- IP address of the requesting client
|
||||
- HTTP method and endpoint path
|
||||
- Response status code
|
||||
- Request timestamp
|
||||
- Response time (latency)
|
||||
|
||||
We do not log the full content of request or response bodies unless required for debugging.
|
||||
|
||||
### 2.2 Account Data (via RapidAPI)
|
||||
|
||||
If you subscribe through RapidAPI, your account data (name, email, billing information) is managed by RapidAPI, not by us. Please review RapidAPI's Privacy Policy at rapidapi.com/privacy.
|
||||
|
||||
### 2.3 Cookies and Tracking
|
||||
|
||||
The API endpoints themselves do not use cookies. Our documentation site (`docs.leeworks.dev`) may use minimal session cookies for navigation only — no analytics or tracking cookies.
|
||||
|
||||
## 3. How We Use Your Data
|
||||
|
||||
We use collected data to monitor API health, detect and prevent abuse, debug issues, generate aggregate usage statistics (anonymized), and respond to support requests.
|
||||
|
||||
**We do not sell your personal data to third parties. Ever.**
|
||||
|
||||
## 4. Data Retention
|
||||
|
||||
| Data Type | Retention Period |
|
||||
|-----------|-----------------|
|
||||
| Request logs (IP + endpoint) | 90 days |
|
||||
| Aggregated usage metrics | 12 months |
|
||||
| Billing records (via RapidAPI) | Per RapidAPI policy |
|
||||
|
||||
## 5. Data Sharing
|
||||
|
||||
We share data only with RapidAPI for billing and subscription management, when required by law, and with hosting infrastructure providers under data processing agreements.
|
||||
|
||||
## 6. Security
|
||||
|
||||
We take reasonable technical and organizational measures to protect your data including HTTPS-only API keys, restricted log access, and Kubernetes RBAC and network policies.
|
||||
|
||||
## 7. Your Rights
|
||||
|
||||
Depending on your jurisdiction, you may have rights to access, delete, or restrict processing of your personal data. Contact legal@leeworks.dev — we will respond within 30 days.
|
||||
|
||||
## 8. Changes to This Policy
|
||||
|
||||
We may update this Privacy Policy periodically. Continued use of the Services after changes constitutes acceptance.
|
||||
|
||||
## 9. Contact
|
||||
|
||||
For privacy inquiries: **legal@leeworks.dev**
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
layout: ../../layouts/Base.astro
|
||||
title: Terms of Service
|
||||
description: leeworks.dev API Terms of Service
|
||||
---
|
||||
|
||||
# Terms of Service
|
||||
|
||||
**Effective Date:** 2026-05-24
|
||||
**Contact:** legal@leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By accessing or using any API offered by leeworks.dev ("Services"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.
|
||||
|
||||
## 2. Description of Services
|
||||
|
||||
leeworks.dev provides data API services including:
|
||||
- ZIP Enrichment API (`zip.leeworks.dev`)
|
||||
- Holidays API (`holidays.leeworks.dev`)
|
||||
- Air Quality API (`aqi.leeworks.dev`)
|
||||
|
||||
These APIs are offered via RapidAPI and directly. Access requires a valid API key.
|
||||
|
||||
## 3. API Usage Limits
|
||||
|
||||
- Each plan has defined rate limits (requests per minute and per month). Exceeding your plan's limits will result in HTTP 429 responses.
|
||||
- You must not circumvent rate limiting through multiple accounts, shared keys, or other technical means.
|
||||
- Free and Basic plan users are limited to non-commercial use unless explicitly stated otherwise.
|
||||
|
||||
## 4. Prohibited Use
|
||||
|
||||
You may not use the Services to:
|
||||
- Resell or redistribute the API data or API access without written permission
|
||||
- Scrape, download, or replicate the underlying dataset in bulk
|
||||
- Build a competing API product using our data
|
||||
- Violate any applicable laws, including data privacy regulations
|
||||
- Harass, harm, or interfere with other users or our infrastructure
|
||||
|
||||
See also the [Acceptable Use Policy](/legal/acceptable-use-policy).
|
||||
|
||||
## 5. Account Registration and Security
|
||||
|
||||
- You are responsible for keeping your API key confidential.
|
||||
- You are responsible for all activity under your API key.
|
||||
- Notify us immediately at legal@leeworks.dev if you suspect unauthorized use.
|
||||
|
||||
## 6. Payment and Billing
|
||||
|
||||
- Paid plans are billed through RapidAPI according to their billing terms.
|
||||
- Refunds are handled at our discretion on a case-by-case basis. Contact legal@leeworks.dev within 7 days of a charge.
|
||||
- We reserve the right to change pricing with 30 days' notice.
|
||||
|
||||
## 7. Data Accuracy Disclaimer
|
||||
|
||||
The data provided by leeworks.dev APIs is sourced from public datasets. We make no warranty as to the accuracy, completeness, or fitness for any particular purpose. You use the data at your own risk.
|
||||
|
||||
## 8. Service Availability
|
||||
|
||||
- We target 99.9% uptime but make no formal SLA guarantee on free or Basic plans.
|
||||
- We reserve the right to take the service down for maintenance with or without notice.
|
||||
- See status.leeworks.dev for real-time uptime information.
|
||||
|
||||
## 9. Intellectual Property
|
||||
|
||||
- The APIs, documentation, and underlying software are the intellectual property of leeworks.dev.
|
||||
- Response data may be used in your own products subject to these Terms.
|
||||
- You may not claim ownership of the data or present it as proprietary to you.
|
||||
|
||||
## 10. Termination
|
||||
|
||||
We may suspend or terminate your access to the Services immediately, without prior notice, for violation of these Terms, suspected abuse or fraud, or non-payment of applicable fees.
|
||||
|
||||
## 11. Limitation of Liability
|
||||
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LEEWORKS.DEV SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING LOSS OF PROFITS, DATA, OR BUSINESS.
|
||||
|
||||
## 12. Changes to Terms
|
||||
|
||||
We may modify these Terms at any time. Continued use of the Services after changes constitutes acceptance.
|
||||
|
||||
## 13. Contact
|
||||
|
||||
Questions about these Terms? Contact us at: **legal@leeworks.dev**
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="API Pricing" description="API Pricing — leeworks.dev — compare plans for ZIP Enrichment, Holidays, Air Quality, and VIN Decoder APIs">
|
||||
<style>
|
||||
.pricing-hero { padding: 4rem 2rem 2rem; text-align: center; }
|
||||
.pricing-hero h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(135deg, #90cdf4, #667eea); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 1rem; }
|
||||
.pricing-hero p { font-size: 1.15rem; color: #a0aec0; max-width: 600px; margin: 0 auto 2rem; }
|
||||
.pricing-wrap { padding: 1rem 2rem 3rem; max-width: 1100px; margin: 0 auto; overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; background: #1a1d27; border-radius: 12px; overflow: hidden; min-width: 640px; }
|
||||
caption { caption-side: top; text-align: left; font-size: 0.875rem; color: #718096; padding: 0.75rem 1rem; font-style: italic; }
|
||||
thead tr { background: #2d3748; }
|
||||
th, td { padding: 0.9rem 1.25rem; text-align: left; border-bottom: 1px solid #2d3748; }
|
||||
th { font-weight: 700; color: #90cdf4; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
td { color: #e2e8f0; font-size: 0.95rem; }
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: #232738; }
|
||||
.tier-name { font-weight: 600; color: #fff; }
|
||||
.tier-price { color: #9ae6b4; font-weight: 600; }
|
||||
.free-price { color: #68d391; }
|
||||
.cta-row { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin: 2.5rem 0 1rem; }
|
||||
.cta-btn { display: inline-block; background: #667eea; color: #fff; padding: 0.65rem 1.5rem; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 0.95rem; }
|
||||
.cta-btn:hover { background: #5a67d8; }
|
||||
.disclaimer { text-align: center; color: #718096; font-size: 0.85rem; margin-top: 2rem; padding: 0 2rem; }
|
||||
</style>
|
||||
|
||||
<div class="pricing-hero">
|
||||
<h1>API Pricing</h1>
|
||||
<p>Simple, transparent pricing for all four APIs. Start free — upgrade when you need more.</p>
|
||||
</div>
|
||||
|
||||
<div class="pricing-wrap">
|
||||
<table>
|
||||
<caption>All plans are monthly. Requests reset at the start of each billing cycle.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Tier</th>
|
||||
<th scope="col">Price / mo</th>
|
||||
<th scope="col">ZIP Enrichment</th>
|
||||
<th scope="col">Holidays</th>
|
||||
<th scope="col">Air Quality</th>
|
||||
<th scope="col">VIN Decoder</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tier-name">Free</td>
|
||||
<td class="tier-price free-price">$0</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tier-name">Basic</td>
|
||||
<td class="tier-price">$9</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tier-name">Pro</td>
|
||||
<td class="tier-price">$19</td>
|
||||
<td>25,000 req/mo · 200 req/min</td>
|
||||
<td>20,000 req/mo · 200 req/min</td>
|
||||
<td>20,000 req/mo · 200 req/min</td>
|
||||
<td>20,000 req/mo · 200 req/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tier-name">Ultra</td>
|
||||
<td class="tier-price">$49</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="cta-row">
|
||||
<a href="#" class="cta-btn">ZIP Enrichment on RapidAPI</a>
|
||||
<a href="#" class="cta-btn">Holidays on RapidAPI</a>
|
||||
<a href="#" class="cta-btn">Air Quality on RapidAPI</a>
|
||||
<a href="/vin-decoder" class="cta-btn">VIN Decoder Docs</a>
|
||||
</div>
|
||||
|
||||
<p class="disclaimer">Subscriptions managed via RapidAPI marketplace. Prices shown in USD. Rate limits apply per API key.</p>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
|
||||
const apiName = 'vin-decoder';
|
||||
const title = 'VIN Decoder API';
|
||||
const description = 'Decode any 17-character VIN into make, model, year, trim, engine, body class, and more. Powered by the NHTSA vPIC public-domain database.';
|
||||
---
|
||||
<Base title={title} description={description}>
|
||||
<style>
|
||||
#redoc-container { background: #fff; }
|
||||
</style>
|
||||
<div id="redoc-container"></div>
|
||||
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
|
||||
// Load Redoc from CDN
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
|
||||
script.onload = function () {
|
||||
Redoc.init(specUrl, {
|
||||
theme: {
|
||||
colors: { primary: { main: '#667eea' } },
|
||||
typography: { fontFamily: 'system-ui, sans-serif' },
|
||||
},
|
||||
}, document.getElementById('redoc-container'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
|
||||
const apiName = 'zip-enrichment';
|
||||
const titles: Record<string, string> = {
|
||||
'zip-enrichment': 'ZIP Enrichment API',
|
||||
'holidays': 'Holidays API',
|
||||
'air-quality': 'Air Quality API',
|
||||
};
|
||||
const title = titles[apiName];
|
||||
---
|
||||
<Base title={title} description={`${title} — OpenAPI documentation`}>
|
||||
<style>
|
||||
#redoc-container { background: #fff; }
|
||||
</style>
|
||||
<div id="redoc-container"></div>
|
||||
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
|
||||
// Load Redoc from CDN
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
|
||||
script.onload = function () {
|
||||
Redoc.init(specUrl, {
|
||||
theme: {
|
||||
colors: { primary: { main: '#667eea' } },
|
||||
typography: { fontFamily: 'system-ui, sans-serif' },
|
||||
},
|
||||
}, document.getElementById('redoc-container'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
+129
-126
@@ -1,158 +1,161 @@
|
||||
# Cluster Audit
|
||||
# Cluster State Audit
|
||||
|
||||
**Date:** 2026-05-25
|
||||
**Author:** AI-Engineer (agent cycle)
|
||||
**Scope:** Kubernetes cluster `testing1` — nodes, namespaces, ingress, Flux state
|
||||
**Closes:** leeworks-agents/api-company#26
|
||||
|
||||
---
|
||||
|
||||
> **Note:** This audit was compiled from available cluster state data (STATUS.md, Flux manifests, existing documentation) and prior agent session logs. Direct `kubectl` access is unavailable from the agent container. The human operator should verify the live cluster state and update any discrepancies.
|
||||
**Date:** 2026-05-19
|
||||
**Cluster:** testing1/first-cluster (Talos Linux)
|
||||
**Audited by:** Repo Manager (agent)
|
||||
|
||||
---
|
||||
|
||||
## Nodes
|
||||
|
||||
Based on STATUS.md and prior audit sessions:
|
||||
3-node control-plane cluster. No dedicated worker nodes.
|
||||
|
||||
| Node Role | IP Address | Status | Notes |
|
||||
|---------------|------------|---------|--------------------|
|
||||
| Control Plane | 10.0.1.3 | Ready | Talos Linux |
|
||||
| Control Plane | 10.0.1.4 | Ready | Talos Linux |
|
||||
| Control Plane | 10.0.1.5 | Ready | Talos Linux |
|
||||
| Worker(s) | TBD | Unknown | `testing1` cluster |
|
||||
|
||||
**To verify:**
|
||||
```bash
|
||||
kubectl get nodes -o wide
|
||||
```
|
||||
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
|
||||
cp-0 Ready control-plane 84d v1.33.0 10.0.1.3 <none> Talos (v1.11.5) 6.12.57-talos containerd://2.1.5
|
||||
cp-1 Ready control-plane 84d v1.33.0 10.0.1.4 <none> Talos (v1.11.5) 6.12.57-talos containerd://2.1.5
|
||||
cp-2 Ready control-plane 84d v1.33.0 10.0.1.54 <none> Talos (v1.11.5) 6.12.57-talos containerd://2.1.5
|
||||
```
|
||||
|
||||
**Per-node capacity:** 4 CPU, ~7.7 GiB memory, 110 pods max
|
||||
**Cluster totals:** 12 CPU, ~23.1 GiB memory, 330 pods max
|
||||
|
||||
---
|
||||
|
||||
## Namespaces
|
||||
|
||||
| Namespace | Purpose | Status |
|
||||
|------------------|----------------------------------------------|----------|
|
||||
| `kube-system` | Core Kubernetes components | Active |
|
||||
| `flux-system` | FluxCD controllers and sources | Active |
|
||||
| `ingress-nginx` | NGINX ingress controller | Active |
|
||||
| `cert-manager` | Certificate management (Let's Encrypt) | Active |
|
||||
| `gitea` | Gitea source control / container registry | Active |
|
||||
| `monitoring` | Prometheus + Grafana + Gatus (pending Flux) | Staged |
|
||||
| `gitea-runner` | Gitea Actions runner (pending Flux) | Staged |
|
||||
| `docs-site` | Astro docs site (pending Flux) | Staged |
|
||||
| `zip-enrichment` | ZIP Enrichment API service (future) | Not yet |
|
||||
| `holidays` | Holidays API service (future) | Not yet |
|
||||
| `air-quality` | Air Quality API service (future) | Not yet |
|
||||
21 namespaces total. Workload namespaces (excluding system):
|
||||
|
||||
**To verify:**
|
||||
```bash
|
||||
kubectl get namespaces
|
||||
| Namespace | Age | Purpose |
|
||||
|---|---|---|
|
||||
| agent-company | 59d | Agent company workloads |
|
||||
| authentik | 64d | Identity provider (SSO) |
|
||||
| cert-manager | 64d | TLS certificate management |
|
||||
| coredns | 64d | DNS |
|
||||
| gatus | 64d | Uptime monitoring |
|
||||
| gitea-actions-runner | 48d | CI runner for Gitea |
|
||||
| gitea-mobile | 53d | Gitea mobile app |
|
||||
| logging | 52d | Logging stack (Grafana) |
|
||||
| mail | 62d | Mail services |
|
||||
| metallb-system | 64d | Bare-metal load balancer |
|
||||
| monitoring | 51d | Monitoring stack |
|
||||
| nfs-provisioner | 64d | NFS storage provisioner |
|
||||
| nixos-dev | 64d | NixOS dev environment |
|
||||
| sealed-secrets | 55d | Sealed secrets controller |
|
||||
| sparc | 64d | Sparc application |
|
||||
| traefik | 64d | Ingress / reverse proxy |
|
||||
|
||||
---
|
||||
|
||||
## Ingress Setup
|
||||
|
||||
No standard Kubernetes `Ingress` resources found. The cluster uses **Traefik IngressRoutes** (CRD-based):
|
||||
|
||||
| Namespace | IngressRoute | Age |
|
||||
|---|---|---|
|
||||
| authentik | authentik | 64d |
|
||||
| authentik | authentik-http | 64d |
|
||||
| gatus | gatus | 64d |
|
||||
| gatus | gatus-http | 64d |
|
||||
| gitea-mobile | gitea-mobile | 53d |
|
||||
| gitea-mobile | gitea-mobile-http | 53d |
|
||||
| logging | grafana | 52d |
|
||||
| logging | grafana-http | 52d |
|
||||
| sparc | sparc | 64d |
|
||||
| sparc | sparc-http | 64d |
|
||||
| traefik | traefik-dashboard | 64d |
|
||||
|
||||
---
|
||||
|
||||
## Resource Usage (Headroom)
|
||||
|
||||
> **Note:** `kubectl top nodes` is unavailable -- Metrics API (metrics-server) is not installed. Resource requests/limits from `kubectl describe nodes` are used instead.
|
||||
|
||||
### Per-Node Allocated Resources (Requests)
|
||||
|
||||
| Node | CPU Requests | CPU % | Memory Requests | Memory % |
|
||||
|---|---|---|---|---|
|
||||
| cp-0 | 1710m | 43% | 2738Mi | 37% |
|
||||
| cp-1 | 3360m | 85% | 5950Mi | 81% |
|
||||
| cp-2 | 2100m | 53% | 4018Mi | 54% |
|
||||
|
||||
### Per-Node Limits (for overcommit awareness)
|
||||
|
||||
| Node | CPU Limits | CPU % | Memory Limits | Memory % |
|
||||
|---|---|---|---|---|
|
||||
| cp-0 | 7300m | 184% | 8320Mi | 113% |
|
||||
| cp-1 | 9700m | 245% | 13908Mi | 190% |
|
||||
| cp-2 | 6500m | 164% | 11008Mi | 150% |
|
||||
|
||||
## Headroom
|
||||
|
||||
**WARNING -- The following nodes have less than 20% free capacity by requests:**
|
||||
|
||||
- **cp-1 CPU: 85% requested** -- only 15% headroom. This node is near capacity for CPU requests.
|
||||
- **cp-1 Memory: 81% requested** -- only 19% headroom. This node is near capacity for memory requests.
|
||||
|
||||
**ADVISORY -- Overcommit risk:**
|
||||
|
||||
All three nodes have CPU and memory limits exceeding 100%. This means actual usage spikes could cause OOM kills or CPU throttling. This is common in non-production clusters but should be monitored.
|
||||
|
||||
- cp-0: CPU limits at 184%, memory limits at 113%
|
||||
- cp-1: CPU limits at 245%, memory limits at 190%
|
||||
- cp-2: CPU limits at 164%, memory limits at 150%
|
||||
|
||||
**Recommendation:** Install metrics-server to enable `kubectl top` and real-time resource monitoring. Consider adding a dedicated worker node if workloads continue to grow, as cp-1 is already heavily loaded.
|
||||
|
||||
---
|
||||
|
||||
## Flux Kustomizations
|
||||
|
||||
All kustomizations are reconciled and ready.
|
||||
|
||||
```
|
||||
NAMESPACE NAME REVISION SUSPENDED READY MESSAGE
|
||||
flux-system apps main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
flux-system authentik main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
flux-system cert-config main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
flux-system flux-config main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
flux-system infrastructure main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
flux-system traefik main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
flux-system traefik-config main@sha1:c48d8414 False True Applied revision: main@sha1:c48d8414
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ingress Rules
|
||||
## Flux Helm Releases
|
||||
|
||||
| Host | Service / Namespace | TLS | Status |
|
||||
|-------------------------|------------------------------|---------------|------------------|
|
||||
| `gitea.leeworks.dev` | gitea / gitea | Let's Encrypt | Active |
|
||||
| `registry.leeworks.dev` | gitea / gitea | Let's Encrypt | Pending DNS/pkg |
|
||||
| `grafana.leeworks.dev` | grafana / monitoring | Let's Encrypt | Pending Flux |
|
||||
| `status.leeworks.dev` | gatus / monitoring | Let's Encrypt | Pending Flux |
|
||||
| `docs.leeworks.dev` | docs-site / docs-site | Let's Encrypt | Pending Flux |
|
||||
| `zip.leeworks.dev` | zip-enrichment / zip-enrich | Let's Encrypt | Not deployed |
|
||||
| `holidays.leeworks.dev` | holidays / holidays | Let's Encrypt | Not deployed |
|
||||
| `aqi.leeworks.dev` | air-quality / air-quality | Let's Encrypt | Not deployed |
|
||||
All Helm releases are reconciled and ready.
|
||||
|
||||
**To verify:**
|
||||
```bash
|
||||
kubectl get ingress -A
|
||||
# To get ingress IP:
|
||||
kubectl get svc -n ingress-nginx ingress-nginx-controller \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'
|
||||
```
|
||||
NAMESPACE NAME REVISION SUSPENDED READY MESSAGE
|
||||
authentik authentik 2026.2.3 False True Helm upgrade succeeded
|
||||
cert-manager cert-manager v1.14.7 False True Helm install succeeded
|
||||
sealed-secrets sealed-secrets 2.18.5 False True Helm upgrade succeeded
|
||||
traefik traefik 28.3.0 False True Helm upgrade succeeded
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flux State
|
||||
## Storage Classes
|
||||
|
||||
### GitRepository Sources
|
||||
|
||||
| Name | URL | Branch | Ready | Notes |
|
||||
|---------------|-------------------------------------------------------------|--------|-------------|--------------------------------------------------------------------------|
|
||||
| `flux-system` | `ssh://git@gitea.leeworks.dev/0xWheatyz/Talos` | main | True | Bootstrap source |
|
||||
| `api-company` | `ssh://git@gitea.leeworks.dev/leeworks-agents/api-company` | main | **PENDING** | Manifests staged at `flux/api-company-source/` — needs Talos merge (#2) |
|
||||
|
||||
**To verify:**
|
||||
```bash
|
||||
flux get sources git -A
|
||||
```
|
||||
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
|
||||
nfs-client (default) k8s-sigs.io/nfs-subdir-external-provisioner Delete Immediate false 64d
|
||||
```
|
||||
|
||||
### Kustomizations
|
||||
|
||||
| Name | Path | Ready | Notes |
|
||||
|---------------|---------------------------------------------|-------------|--------------------------------|
|
||||
| `flux-system` | `testing1/first-cluster/cluster/flux/` | True | Bootstrap kustomization |
|
||||
| `api-company` | `flux/` | **PENDING** | Blocked on issue #2 (Talos PR) |
|
||||
|
||||
**To verify:**
|
||||
```bash
|
||||
flux get kustomizations -A
|
||||
```
|
||||
|
||||
### HelmReleases
|
||||
|
||||
| Name | Namespace | Chart | Ready | Notes |
|
||||
|-------------------------|---------------|-----------------------|-----------------|----------------------------------------------|
|
||||
| `gitea-act-runner` | gitea-runner | gitea-act-runner | **NOT READY** | Needs runner token secret (#3) |
|
||||
| `kube-prometheus-stack` | monitoring | kube-prometheus-stack | **NOT READY** | Needs Flux wiring + Grafana secret (#7) |
|
||||
| `gatus` | monitoring | gatus (TrueCharts) | **NOT READY** | Needs Flux wiring + Slack secret (#8) |
|
||||
| `docs-site` | docs-site | raw (bedag) | **NOT READY** | Needs Flux wiring + DNS record (#30) |
|
||||
|
||||
**To verify:**
|
||||
```bash
|
||||
flux get helmreleases -A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NOT-READY Objects — Action Required by Human Operator
|
||||
|
||||
| Object | Blocked By | Required Action |
|
||||
|---------------------------------|------------|-------------------------------------------------------------------------------------------------|
|
||||
| `GitRepository/api-company` | Issue #2 | Add `flux/api-company-source/` manifests to `0xWheatyz/Talos` at `testing1/first-cluster/cluster/flux/` |
|
||||
| `HelmRelease/gitea-act-runner` | Issue #3 | Create `gitea-runner-token` secret in `gitea-runner` namespace |
|
||||
| `HelmRelease/kube-prometheus-stack` | Issue #7 | Create `grafana-admin` secret in `monitoring` namespace |
|
||||
| `HelmRelease/gatus` | Issue #8 | Create Slack webhook secret in `monitoring` namespace (optional for alerting) |
|
||||
| `HelmRelease/docs-site` | Issue #30 | Enable Gitea packages + add DNS A record `docs.leeworks.dev` → cluster ingress IP |
|
||||
| `registry.leeworks.dev` | Issue #4 | Enable `[packages] ENABLED=true` in Gitea app.ini + DNS A record → cluster ingress IP |
|
||||
|
||||
---
|
||||
|
||||
## Flux Manifest Validation
|
||||
|
||||
```bash
|
||||
kustomize build flux/
|
||||
# Exit 0 — all manifests syntactically valid
|
||||
```
|
||||
|
||||
Validated directories:
|
||||
- `flux/api-company-source/` — GitRepository + Kustomization for this repo
|
||||
- `flux/gitea-runner/` — Namespace + HelmRelease for act-runner
|
||||
- `flux/monitoring/` — Namespace + kube-prometheus-stack HelmRelease + Gatus HelmRelease
|
||||
- `flux/docs-site/` — Namespace + HelmRelease (bedag/raw chart) for Astro site
|
||||
Single storage class using NFS. Volume expansion is **not** enabled. Reclaim policy is **Delete** (PVCs are cleaned up on release).
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Status |
|
||||
|----------------|------------------------------------------------|
|
||||
| Cluster health | ✅ 3-node Talos control plane, healthy |
|
||||
| Flux bootstrap | ✅ Active, reconciling from `0xWheatyz/Talos` |
|
||||
| api-company GitOps wiring | ⚠️ PENDING — PR to Talos required (issue #2) |
|
||||
| Services live | Gitea |
|
||||
| Services staged | gitea-act-runner, Prometheus/Grafana, Gatus, docs-site |
|
||||
| Services future | zip-enrichment, holidays, air-quality |
|
||||
| Human blockers | 6 items (see table above) |
|
||||
| Item | Status |
|
||||
|---|---|
|
||||
| Nodes | 3x control-plane, all Ready, Talos v1.11.5, K8s v1.33.0 |
|
||||
| Namespaces | 21 total (16 workload, 5 system) |
|
||||
| Ingress | Traefik IngressRoutes (11 routes across 6 namespaces) |
|
||||
| Flux | 7 kustomizations, 4 Helm releases -- all healthy |
|
||||
| Storage | NFS-backed default StorageClass |
|
||||
| Headroom | cp-1 is near capacity (85% CPU, 81% memory requests) |
|
||||
| Metrics | metrics-server NOT installed -- no real-time usage data |
|
||||
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
# DNS Configuration
|
||||
|
||||
**Last updated:** 2026-05-30
|
||||
**Status:** Planned (Phase 6 pre-launch)
|
||||
|
||||
---
|
||||
|
||||
## DNS Provider
|
||||
|
||||
DNS for `leeworks.dev` is managed externally (by the human operator via their registrar/DNS provider). The agent cannot directly create DNS records. This document tracks the required records for human operator action.
|
||||
|
||||
---
|
||||
|
||||
## Required Records
|
||||
|
||||
All records should point to the cluster ingress IP. To find the current ingress IP:
|
||||
|
||||
```bash
|
||||
kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
|
||||
```
|
||||
|
||||
| Subdomain | Type | Target | Purpose | TLS Required |
|
||||
|-----------|------|--------|---------|-------------|
|
||||
| `zip.leeworks.dev` | A | `<cluster-ingress-ip>` | ZIP Enrichment API | Yes (cert-manager) |
|
||||
| `holidays.leeworks.dev` | A | `<cluster-ingress-ip>` | Holidays API | Yes (cert-manager) |
|
||||
| `aqi.leeworks.dev` | A | `<cluster-ingress-ip>` | Air Quality API | Yes (cert-manager) |
|
||||
| `docs.leeworks.dev` | A | `<cluster-ingress-ip>` | Documentation site | Yes (cert-manager) |
|
||||
| `status.leeworks.dev` | A | `<cluster-ingress-ip>` | Gatus status page | Yes (cert-manager) |
|
||||
| `registry.leeworks.dev` | A | `<cluster-ingress-ip>` | Container registry (Gitea) | Yes (cert-manager) |
|
||||
| `grafana.leeworks.dev` | A | `<cluster-ingress-ip>` | Grafana (internal/restricted) | Yes (cert-manager) |
|
||||
| `vin.leeworks.dev` | A | `<cluster-ingress-ip>` | VIN Decoder API | Yes (cert-manager) |
|
||||
|
||||
---
|
||||
|
||||
## TLS Certificate Management
|
||||
|
||||
TLS certificates are issued automatically by **cert-manager** using Let's Encrypt (ACME HTTP-01 or DNS-01 challenge).
|
||||
|
||||
### Prerequisites
|
||||
- cert-manager deployed in the cluster (part of Talos setup)
|
||||
- A `ClusterIssuer` configured for Let's Encrypt
|
||||
|
||||
### ClusterIssuer (Let's Encrypt Production)
|
||||
|
||||
```yaml
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: legal@leeworks.dev
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: nginx
|
||||
```
|
||||
|
||||
### Example Ingress with TLS
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: zip-enrichment-ingress
|
||||
namespace: zip-enrichment
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- zip.leeworks.dev
|
||||
secretName: zip-tls
|
||||
rules:
|
||||
- host: zip.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: zip-enrichment
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Steps
|
||||
|
||||
After DNS records are created:
|
||||
|
||||
```bash
|
||||
# Check DNS resolution
|
||||
dig zip.leeworks.dev +short
|
||||
dig holidays.leeworks.dev +short
|
||||
dig aqi.leeworks.dev +short
|
||||
dig docs.leeworks.dev +short
|
||||
dig status.leeworks.dev +short
|
||||
dig registry.leeworks.dev +short
|
||||
dig grafana.leeworks.dev +short
|
||||
dig vin.leeworks.dev +short
|
||||
|
||||
# Check TLS certificates (once services are deployed)
|
||||
curl -v https://zip.leeworks.dev/health 2>&1 | grep -E "SSL|certificate|issuer"
|
||||
|
||||
# Check cert-manager issued certs
|
||||
kubectl get certificates -A
|
||||
|
||||
# Expect HTTP 200 on health endpoints
|
||||
for host in zip.leeworks.dev holidays.leeworks.dev aqi.leeworks.dev; do
|
||||
echo -n "$host: "
|
||||
curl -s -o /dev/null -w "%{http_code}" https://$host/health
|
||||
echo
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Action Required (Human Operator)
|
||||
|
||||
The following actions require human operator access to the DNS provider:
|
||||
|
||||
1. Log into the DNS provider managing `leeworks.dev`
|
||||
2. Find the cluster ingress IP: `kubectl get svc -n ingress-nginx ingress-nginx-controller`
|
||||
3. Create/update the 8 A records listed in the table above
|
||||
4. Verify propagation: `dig +trace zip.leeworks.dev`
|
||||
|
||||
DNS propagation typically takes 5–60 minutes.
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
- [ ] Cluster ingress IP confirmed
|
||||
- [ ] `zip.leeworks.dev` → DNS record created
|
||||
- [ ] `holidays.leeworks.dev` → DNS record created
|
||||
- [ ] `aqi.leeworks.dev` → DNS record created
|
||||
- [ ] `docs.leeworks.dev` → DNS record created
|
||||
- [ ] `status.leeworks.dev` → DNS record created
|
||||
- [ ] `registry.leeworks.dev` → DNS record created
|
||||
- [ ] `grafana.leeworks.dev` → DNS record created
|
||||
- [ ] `vin.leeworks.dev` → DNS record created
|
||||
- [ ] TLS certificates issued and valid for all 8 subdomains
|
||||
@@ -1,359 +0,0 @@
|
||||
# Launch Announcement Copy
|
||||
|
||||
Ready-to-copy marketing text for leeworks.dev API launch day.
|
||||
|
||||
**Status key:**
|
||||
- **DRAFT** — copy is written and ready; needs final RapidAPI URLs inserted once issue #44 is complete
|
||||
- **READY** — all placeholders filled; copy-paste ready to publish
|
||||
|
||||
All sections are currently **DRAFT** pending RapidAPI listing URLs from issue #44.
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Reference
|
||||
|
||||
When issue #44 is complete, replace these placeholders throughout this document:
|
||||
|
||||
| Placeholder | Replace with |
|
||||
|---|---|
|
||||
| `[RAPIDAPI_ZIP_URL]` | RapidAPI listing URL for ZIP Code Enrichment API |
|
||||
| `[RAPIDAPI_HOLIDAYS_URL]` | RapidAPI listing URL for Holidays API |
|
||||
| `[RAPIDAPI_AQI_URL]` | RapidAPI listing URL for Air Quality Index API |
|
||||
| `[RAPIDAPI_PROFILE_URL]` | Your RapidAPI provider profile URL |
|
||||
|
||||
---
|
||||
|
||||
## 1. Hacker News — Show HN Post [DRAFT]
|
||||
|
||||
**Title:**
|
||||
```
|
||||
Show HN: I built 3 free-data APIs on Kubernetes — ZIP enrichment, public holidays, air quality
|
||||
```
|
||||
|
||||
**Body (paste into the "text" field):**
|
||||
```
|
||||
Three small APIs I've been building over the past few months, deployed via Flux GitOps on a self-hosted Kubernetes cluster.
|
||||
|
||||
**What they do:**
|
||||
|
||||
1. ZIP Code Enrichment API — turn any US ZIP code into city, state, county, timezone, area codes, and coordinates. Backed by USPS/Census public data, refreshed monthly. [RAPIDAPI_ZIP_URL]
|
||||
|
||||
2. Public Holidays API — query official public holidays for any country and year. 90+ countries, ISO 3166 codes. Backed by Nager.Date / public government calendars. [RAPIDAPI_HOLIDAYS_URL]
|
||||
|
||||
3. Air Quality Index API — current and historical AQI by city or coordinates. PM2.5, PM10, O3, NO2, SO2, CO. Backed by OpenAQ public dataset. [RAPIDAPI_AQI_URL]
|
||||
|
||||
**Tech stack:** Fastify (Node.js), SQLite (data cache), Flux GitOps on Talos Linux, cert-manager + ingress-nginx, Prometheus + Grafana for metrics, Gatus for status page.
|
||||
|
||||
**Business model:** Free tier (100 req/mo) + paid tiers ($9/$19/$49/mo) on RapidAPI. All three APIs use only public-domain data sources with no redistribution restrictions, so operating costs are cluster hosting only.
|
||||
|
||||
**Why I built this:** I wanted to learn GitOps/Kubernetes end-to-end, build something that generates real revenue, and ship entirely on open data. The stack is overkill for 3 simple APIs — but that's the point.
|
||||
|
||||
Code is private (it's a product), but happy to answer questions about the architecture.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Reddit Posts [DRAFT]
|
||||
|
||||
### r/webdev
|
||||
|
||||
**Title:**
|
||||
```
|
||||
I built a ZIP code enrichment API on public Census data — city, state, county, timezone from a single lookup
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
Been working on a simple utility API for the past few months. ZIP Code Enrichment takes any US ZIP code and returns:
|
||||
|
||||
- City name + state (abbreviation and full name)
|
||||
- County + FIPS code
|
||||
- Timezone (IANA name + UTC offset)
|
||||
- Area codes
|
||||
- Latitude/longitude centroid
|
||||
- ZIP type (standard, PO Box, military, unique)
|
||||
|
||||
**The data source** is a monthly-refreshed dataset from USPS/Census Bureau — entirely public domain, no scraping.
|
||||
|
||||
**Code example:**
|
||||
|
||||
```javascript
|
||||
const response = await fetch('https://zip.leeworks.dev/v1/lookup?zip=10001', {
|
||||
headers: { 'X-RapidAPI-Proxy-Secret': process.env.RAPIDAPI_KEY }
|
||||
});
|
||||
const data = await response.json();
|
||||
// { zip: "10001", city: "New York", state: "NY", county: "New York County",
|
||||
// timezone: "America/New_York", lat: 40.7484, lon: -73.9967, ... }
|
||||
```
|
||||
|
||||
Free tier is 100 requests/month. Paid plans start at $9/mo for 10,000 req/mo.
|
||||
|
||||
RapidAPI listing: [RAPIDAPI_ZIP_URL]
|
||||
|
||||
Happy to answer any questions about the stack (Fastify + SQLite + Kubernetes/Flux).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### r/SideProject
|
||||
|
||||
**Title:**
|
||||
```
|
||||
Launched 3 data APIs on RapidAPI — ZIP enrichment, public holidays, air quality. $0 → targeting $100/mo MRR
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
Finally shipped the thing I've been building on weekends for the past few months.
|
||||
|
||||
**What I built:**
|
||||
Three utility APIs on RapidAPI, all backed by free public-domain data:
|
||||
|
||||
1. **ZIP Code Enrichment** — city/state/county/timezone from a ZIP code ([RAPIDAPI_ZIP_URL])
|
||||
2. **Public Holidays** — official holidays for 90+ countries ([RAPIDAPI_HOLIDAYS_URL])
|
||||
3. **Air Quality Index** — current + historical AQI by city or coordinates ([RAPIDAPI_AQI_URL])
|
||||
|
||||
**Stack:** Fastify + SQLite + Kubernetes (Talos Linux) + Flux GitOps + Prometheus/Grafana
|
||||
|
||||
**Business model:**
|
||||
- Free tier: 100 req/month (marketing + trial)
|
||||
- Basic: $9/mo — 10,000 req/mo
|
||||
- Pro: $19/mo — 50,000 req/mo
|
||||
- Ultra: $49/mo — 250,000 req/mo
|
||||
|
||||
**Data cost: $0.** All three APIs use US government / OpenAQ public datasets with no licensing fees.
|
||||
|
||||
**Revenue so far:** $0 (launched today). Target: $100/mo net within 90 days, which is roughly 12 Basic subscribers across all three APIs.
|
||||
|
||||
The whole build — from first commit to Kubernetes deployment — is documented in a private research log. Happy to share architecture details.
|
||||
|
||||
What would you do differently for the pricing?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### r/learnprogramming
|
||||
|
||||
**Title:**
|
||||
```
|
||||
I used free US government data to build a ZIP code API — here's how the data pipeline works
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
A walkthrough of the data layer behind the ZIP Code Enrichment API I just launched.
|
||||
|
||||
**The problem:** ZIP codes change. Cities merge. New ZIPs are added. Any ZIP lookup service needs to stay fresh.
|
||||
|
||||
**The solution:** A monthly seed script that:
|
||||
1. Downloads the latest US ZIP code dataset from USPS/Census Bureau (public domain)
|
||||
2. Parses and normalizes ~43,000 records
|
||||
3. Inserts into SQLite with upsert logic (new ZIPs added, old ones retired)
|
||||
4. Runs automatically via a Kubernetes CronJob on the 1st of each month
|
||||
|
||||
**The API itself** is a Fastify (Node.js) server that queries SQLite. Cold query: ~5ms. The whole thing runs in a 128MB container.
|
||||
|
||||
**Code snippet** (the seed script core logic):
|
||||
|
||||
```javascript
|
||||
// Fetch and parse Census ZIP dataset
|
||||
const stream = await fetch(CENSUS_ZIP_URL);
|
||||
const records = await parseCSV(stream.body);
|
||||
|
||||
// Upsert into SQLite
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO zips (zip, city, state, county, lat, lon, timezone, type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(zip) DO UPDATE SET
|
||||
city=excluded.city, state=excluded.state,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
`);
|
||||
|
||||
for (const record of records) {
|
||||
stmt.run([record.zip, record.city, record.state, record.county,
|
||||
record.lat, record.lon, record.timezone, record.type]);
|
||||
}
|
||||
```
|
||||
|
||||
The API is live on RapidAPI with a free tier: [RAPIDAPI_ZIP_URL]
|
||||
|
||||
Happy to answer questions about SQLite performance, the seed pipeline, or the Kubernetes/Flux deployment.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Indie Hackers Milestone Post [DRAFT]
|
||||
|
||||
**Title:**
|
||||
```
|
||||
Launched 3 data APIs on RapidAPI: $0 MRR, targeting $100/mo in 90 days
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
### What I built
|
||||
|
||||
Three utility APIs backed entirely by free public-domain data:
|
||||
|
||||
- **ZIP Code Enrichment** — city/state/county/timezone/coordinates from any US ZIP ([RAPIDAPI_ZIP_URL])
|
||||
- **Public Holidays** — official holidays for 90+ countries + year ([RAPIDAPI_HOLIDAYS_URL])
|
||||
- **Air Quality Index** — current + historical AQI by city or coordinates ([RAPIDAPI_AQI_URL])
|
||||
|
||||
### Revenue: $0 → targeting $100/mo
|
||||
|
||||
The $100/mo target is ~12 Basic subscribers ($9/mo) across all three APIs after RapidAPI's 25% cut. Stretch: 4 Pro subscribers ($19/mo each).
|
||||
|
||||
### Stack
|
||||
|
||||
- **API servers:** Fastify (Node.js) + SQLite for data cache
|
||||
- **Infrastructure:** Talos Linux Kubernetes cluster (self-hosted, single-node)
|
||||
- **GitOps:** Flux CD — everything is declared in YAML, zero manual kubectl
|
||||
- **Observability:** Prometheus + Grafana, Gatus status page at status.leeworks.dev
|
||||
- **Data sources:** USPS/Census (ZIP), Nager.Date (Holidays), OpenAQ (AQI) — all public domain, $0 licensing cost
|
||||
|
||||
### What I learned
|
||||
|
||||
1. **GitOps is excellent for solo projects.** Flux means my cluster is always in sync with git. I've done zero manual deploys.
|
||||
2. **SQLite is underrated for read-heavy APIs.** Sub-5ms query times for cached lookups, zero infrastructure overhead vs PostgreSQL.
|
||||
3. **Public-domain data has a moat.** Anyone can build this, but most people don't bother. The data is stable, legal, and free forever.
|
||||
4. **Kubernetes is overkill — and that's fine.** I did it to learn. I now know Talos, Flux, cert-manager, ingress-nginx, Prometheus, Grafana from first principles.
|
||||
|
||||
### What's next
|
||||
|
||||
- Monitor first 30 days for subscriber growth
|
||||
- Build VIN Decoder as API #4 (NHTSA vPIC data, also public domain)
|
||||
- Add batch endpoints to ZIP and Holidays
|
||||
|
||||
Would love feedback on pricing — is $9/mo entry too high or too low for a utility API with a free tier?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Product Hunt Listing [DRAFT]
|
||||
|
||||
### Tagline (60 chars max)
|
||||
```
|
||||
3 utility APIs on public data — ZIP, Holidays, Air Quality
|
||||
```
|
||||
*(58 characters ✓)*
|
||||
|
||||
### Description (260 chars max)
|
||||
```
|
||||
Look up ZIP codes, public holidays for 90+ countries, and air quality index data — all via clean REST APIs backed by free government datasets. Free tier included. No API keys to generate — available on RapidAPI.
|
||||
```
|
||||
*(211 characters ✓)*
|
||||
|
||||
### First Comment (Maker Note)
|
||||
```
|
||||
Hey Product Hunt! 👋
|
||||
|
||||
I'm the developer behind leeworks.dev — three utility APIs I've been building over the past few months:
|
||||
|
||||
**ZIP Code Enrichment** [RAPIDAPI_ZIP_URL]
|
||||
Turn any US ZIP code into city, state, county, timezone, area codes, and GPS coordinates. 43,000+ ZIP codes, refreshed monthly from USPS/Census Bureau public data.
|
||||
|
||||
**Public Holidays API** [RAPIDAPI_HOLIDAYS_URL]
|
||||
Query official public holidays for any country and year. 90+ countries, ISO 3166 codes, backed by government calendar data. Great for payroll software, scheduling tools, and calendar apps.
|
||||
|
||||
**Air Quality Index API** [RAPIDAPI_AQI_URL]
|
||||
Current and historical AQI readings by city or coordinates. PM2.5, PM10, O3, NO2, SO2, CO — backed by the OpenAQ public dataset covering thousands of monitoring stations worldwide.
|
||||
|
||||
**What makes these different:**
|
||||
- All data is 100% public domain — no licensing fees, no terms restrictions
|
||||
- Free tier (100 req/mo) to try before you buy
|
||||
- Paid plans start at $9/mo for 10,000 requests/month
|
||||
- Running on Kubernetes with Prometheus monitoring and a public status page at status.leeworks.dev
|
||||
|
||||
Happy to answer questions about the data sources, the tech stack (Fastify + SQLite + Flux GitOps), or the pricing model. Thanks for checking it out!
|
||||
```
|
||||
|
||||
### Gallery / Screenshot URL Placeholders
|
||||
```
|
||||
1. docs-site homepage: https://docs.leeworks.dev (screenshot)
|
||||
2. Grafana dashboard: https://grafana.leeworks.dev (screenshot)
|
||||
3. status.leeworks.dev (screenshot)
|
||||
4. Example API response (ZIP lookup): code screenshot
|
||||
5. RapidAPI listing page: [RAPIDAPI_PROFILE_URL] (screenshot)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Email Waitlist Message [DRAFT]
|
||||
|
||||
**Subject line:**
|
||||
```
|
||||
leeworks.dev APIs are live — here's your free tier access
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
Hi there,
|
||||
|
||||
The three APIs I've been building are now live on RapidAPI. Here's what's available and how to get started:
|
||||
|
||||
---
|
||||
|
||||
**ZIP Code Enrichment API**
|
||||
Turn any US ZIP code into city, state, county, timezone, area codes, and GPS coordinates — in a single API call.
|
||||
→ [RAPIDAPI_ZIP_URL]
|
||||
|
||||
**Public Holidays API**
|
||||
Query official public holidays for any country and year. 90+ countries, ISO 3166 codes.
|
||||
→ [RAPIDAPI_HOLIDAYS_URL]
|
||||
|
||||
**Air Quality Index API**
|
||||
Current and historical AQI by city or coordinates. PM2.5, PM10, O3, NO2, SO2, CO.
|
||||
→ [RAPIDAPI_AQI_URL]
|
||||
|
||||
---
|
||||
|
||||
**How to try for free:**
|
||||
1. Click any link above
|
||||
2. Subscribe to the **Free tier** (100 requests/month, no credit card needed)
|
||||
3. Copy your RapidAPI key from the dashboard
|
||||
4. Make your first request — full docs at https://docs.leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
**Quick start (ZIP enrichment):**
|
||||
|
||||
```bash
|
||||
curl "https://zip.leeworks.dev/v1/lookup?zip=90210" \
|
||||
-H "X-RapidAPI-Key: YOUR_KEY" \
|
||||
-H "X-RapidAPI-Host: zip-enrichment.p.rapidapi.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Want more than 100 requests/month?**
|
||||
Paid plans start at $9/mo for 10,000 requests. See the full pricing table on each API's RapidAPI listing page.
|
||||
|
||||
Questions? Reply to this email or open an issue at https://docs.leeworks.dev/support.
|
||||
|
||||
Thanks for your interest,
|
||||
Wyatt
|
||||
leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
*You're receiving this because you signed up for early access. To unsubscribe, reply with "unsubscribe".*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Publishing
|
||||
|
||||
Before changing any section from DRAFT to READY:
|
||||
|
||||
- [ ] Issue #44 complete — RapidAPI listing URLs obtained
|
||||
- [ ] Replace all `[RAPIDAPI_ZIP_URL]` placeholders
|
||||
- [ ] Replace all `[RAPIDAPI_HOLIDAYS_URL]` placeholders
|
||||
- [ ] Replace all `[RAPIDAPI_AQI_URL]` placeholders
|
||||
- [ ] Replace all `[RAPIDAPI_PROFILE_URL]` placeholders
|
||||
- [ ] Verify code examples work against live endpoints
|
||||
- [ ] Confirm free tier limit is accurate (currently documented as 100 req/mo)
|
||||
- [ ] Confirm all three APIs pass pre-launch-checklist.md
|
||||
- [ ] Product Hunt gallery screenshots captured
|
||||
- [ ] Email list exported from whatever signup form was used
|
||||
|
||||
Once all items above are checked, update the Status key at the top of this document from DRAFT to READY for each section.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Acceptable Use Policy
|
||||
|
||||
**Effective Date:** 2026-05-24
|
||||
**Contact:** legal@leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This Acceptable Use Policy ("AUP") defines the rules for using leeworks.dev APIs. It applies to all users regardless of plan. Violations may result in immediate account suspension.
|
||||
|
||||
## 2. Rate Limits and Abuse
|
||||
|
||||
### 2.1 Respect Your Plan Limits
|
||||
|
||||
Each subscription plan includes defined rate limits:
|
||||
|
||||
| Plan | Requests/min | Requests/month |
|
||||
|------|-------------|---------------|
|
||||
| Free | 10 | 500 |
|
||||
| Basic | 60 | 10,000 |
|
||||
| Pro | 300 | 100,000 |
|
||||
| Ultra | 1,000 | 1,000,000 |
|
||||
|
||||
You must not exceed your plan's limits through any means.
|
||||
|
||||
### 2.2 Prohibited Rate Limit Circumvention
|
||||
|
||||
The following are explicitly prohibited:
|
||||
- Using multiple API keys or accounts to aggregate quota
|
||||
- Caching responses for redistribution beyond your own application
|
||||
- Rotating IP addresses to avoid throttling
|
||||
- Using proxies or VPNs specifically to bypass rate limits
|
||||
|
||||
## 3. Prohibited Uses
|
||||
|
||||
### 3.1 Data Scraping and Bulk Download
|
||||
|
||||
You may **not**:
|
||||
- Download or cache the entire dataset backing any API
|
||||
- Make sequential requests designed to reconstruct the underlying database
|
||||
- Use automated tools to systematically extract all available data points
|
||||
|
||||
### 3.2 Resale and Redistribution
|
||||
|
||||
You may **not**:
|
||||
- Resell, sublicense, or redistribute API access to third parties
|
||||
- Build a competing API product that serves our data to others
|
||||
- Offer a "proxy" service that wraps our API for other developers
|
||||
|
||||
### 3.3 Malicious and Illegal Use
|
||||
|
||||
You may **not**:
|
||||
- Use the APIs for any illegal purpose under applicable law
|
||||
- Use the APIs to harass, stalk, or harm any individual
|
||||
- Attempt to compromise the security or integrity of our systems
|
||||
- Reverse-engineer our APIs beyond what's documented in the OpenAPI spec
|
||||
- Use the APIs to generate or distribute spam
|
||||
|
||||
### 3.4 Infrastructure Attacks
|
||||
|
||||
You may **not**:
|
||||
- Perform denial-of-service attacks against our infrastructure
|
||||
- Probe our systems for vulnerabilities without prior written authorization
|
||||
- Exploit bugs or errors to gain elevated access
|
||||
|
||||
## 4. Acceptable Uses
|
||||
|
||||
The following are examples of acceptable use:
|
||||
- Integrating ZIP code, holiday, or air quality data into your own product
|
||||
- Building dashboards, mobile apps, or internal tools
|
||||
- Academic research (within Free plan limits)
|
||||
- Automated data fetching within your plan's rate limits
|
||||
|
||||
## 5. Monitoring and Enforcement
|
||||
|
||||
We continuously monitor API usage for abuse. Automated systems may flag suspicious patterns. Flagged accounts may be:
|
||||
- Throttled further without notice
|
||||
- Required to verify identity
|
||||
- Temporarily suspended pending review
|
||||
- Permanently terminated for serious violations
|
||||
|
||||
## 6. Reporting Abuse
|
||||
|
||||
If you observe misuse of our APIs (e.g., someone redistributing your API key), please report it to **legal@leeworks.dev** immediately.
|
||||
|
||||
## 7. Changes
|
||||
|
||||
We may update this AUP at any time. Significant changes will be announced with an updated effective date. Continued use constitutes acceptance.
|
||||
|
||||
## 8. Contact
|
||||
|
||||
Questions about this policy: **legal@leeworks.dev**
|
||||
@@ -1,96 +0,0 @@
|
||||
# Privacy Policy
|
||||
|
||||
**Effective Date:** 2026-05-24
|
||||
**Contact:** legal@leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
leeworks.dev ("we", "us") operates the ZIP Enrichment, Holidays, and Air Quality APIs. This Privacy Policy describes what data we collect when you use our Services, how we use it, and your rights regarding that data.
|
||||
|
||||
## 2. What Data We Collect
|
||||
|
||||
### 2.1 Request Logs
|
||||
|
||||
When you make API calls, we log:
|
||||
- API key identifier (hashed/truncated — not the full key)
|
||||
- IP address of the requesting client
|
||||
- HTTP method and endpoint path
|
||||
- Response status code
|
||||
- Request timestamp
|
||||
- Response time (latency)
|
||||
|
||||
**We do not log the full content of request or response bodies unless required for debugging.**
|
||||
|
||||
### 2.2 Account Data (via RapidAPI)
|
||||
|
||||
If you subscribe through RapidAPI, your account data (name, email, billing information) is managed by RapidAPI, not by us. Please review [RapidAPI's Privacy Policy](https://rapidapi.com/privacy/).
|
||||
|
||||
### 2.3 Cookies and Tracking
|
||||
|
||||
The API endpoints themselves do not use cookies. Our documentation site (`docs.leeworks.dev`) may use minimal session cookies for navigation only — no analytics or tracking cookies.
|
||||
|
||||
## 3. How We Use Your Data
|
||||
|
||||
We use collected data to:
|
||||
- Monitor API health and uptime
|
||||
- Detect and prevent abuse (rate limit evasion, scraping)
|
||||
- Debug issues and improve service reliability
|
||||
- Generate aggregate usage statistics (anonymized)
|
||||
- Respond to support requests
|
||||
|
||||
**We do not sell your personal data to third parties. Ever.**
|
||||
|
||||
## 4. Data Retention
|
||||
|
||||
| Data Type | Retention Period |
|
||||
|-----------|-----------------|
|
||||
| Request logs (IP + endpoint) | 90 days |
|
||||
| Aggregated usage metrics | 12 months |
|
||||
| Billing records (via RapidAPI) | Per RapidAPI policy |
|
||||
|
||||
After the retention period, logs are automatically deleted.
|
||||
|
||||
## 5. Data Sharing
|
||||
|
||||
We share data only in the following circumstances:
|
||||
- **With RapidAPI**: billing and subscription management
|
||||
- **Legal requirements**: if required by law, court order, or government request
|
||||
- **Service providers**: hosting infrastructure providers (under data processing agreements)
|
||||
|
||||
We do not share raw request logs with any third parties.
|
||||
|
||||
## 6. Security
|
||||
|
||||
We take reasonable technical and organizational measures to protect your data:
|
||||
- API keys are transmitted over HTTPS only
|
||||
- Access to log storage is restricted to authorized personnel
|
||||
- Our cluster uses Kubernetes RBAC and network policies
|
||||
|
||||
However, no system is 100% secure. If you discover a security vulnerability, please report it to legal@leeworks.dev.
|
||||
|
||||
## 7. Your Rights
|
||||
|
||||
Depending on your jurisdiction, you may have rights to:
|
||||
- Access the personal data we hold about you
|
||||
- Request deletion of your data
|
||||
- Object to or restrict processing
|
||||
|
||||
To exercise these rights, contact us at legal@leeworks.dev. We will respond within 30 days.
|
||||
|
||||
## 8. Children's Privacy
|
||||
|
||||
Our Services are not directed at children under 13. We do not knowingly collect data from children. If you believe a child has submitted data, contact us and we will delete it promptly.
|
||||
|
||||
## 9. International Transfers
|
||||
|
||||
Our services are hosted in the United States. By using the Services, you consent to the transfer and processing of your data in the US.
|
||||
|
||||
## 10. Changes to This Policy
|
||||
|
||||
We may update this Privacy Policy periodically. We will notify users of material changes by updating the effective date above and posting a notice. Continued use of the Services after changes constitutes acceptance.
|
||||
|
||||
## 11. Contact
|
||||
|
||||
For privacy inquiries: **legal@leeworks.dev**
|
||||
@@ -1,93 +0,0 @@
|
||||
# Terms of Service
|
||||
|
||||
**Effective Date:** 2026-05-24
|
||||
**Contact:** legal@leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By accessing or using any API offered by leeworks.dev ("Services"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.
|
||||
|
||||
## 2. Description of Services
|
||||
|
||||
leeworks.dev provides data API services including:
|
||||
- ZIP Enrichment API (`zip.leeworks.dev`)
|
||||
- Holidays API (`holidays.leeworks.dev`)
|
||||
- Air Quality API (`aqi.leeworks.dev`)
|
||||
|
||||
These APIs are offered via RapidAPI and directly. Access requires a valid API key.
|
||||
|
||||
## 3. API Usage Limits
|
||||
|
||||
- Each plan has defined rate limits (requests per minute and per month). Exceeding your plan's limits will result in HTTP 429 responses.
|
||||
- You must not circumvent rate limiting through multiple accounts, shared keys, or other technical means.
|
||||
- Free and Basic plan users are limited to non-commercial use unless explicitly stated otherwise.
|
||||
|
||||
## 4. Prohibited Use
|
||||
|
||||
You may not use the Services to:
|
||||
- Resell or redistribute the API data or API access without written permission
|
||||
- Scrape, download, or replicate the underlying dataset in bulk
|
||||
- Build a competing API product using our data
|
||||
- Violate any applicable laws, including data privacy regulations
|
||||
- Harass, harm, or interfere with other users or our infrastructure
|
||||
|
||||
See also the [Acceptable Use Policy](./acceptable-use-policy.md).
|
||||
|
||||
## 5. Account Registration and Security
|
||||
|
||||
- You are responsible for keeping your API key confidential.
|
||||
- You are responsible for all activity under your API key.
|
||||
- Notify us immediately at legal@leeworks.dev if you suspect unauthorized use.
|
||||
|
||||
## 6. Payment and Billing
|
||||
|
||||
- Paid plans are billed through RapidAPI according to their billing terms.
|
||||
- Refunds are handled at our discretion on a case-by-case basis. Contact legal@leeworks.dev within 7 days of a charge.
|
||||
- We reserve the right to change pricing with 30 days' notice.
|
||||
|
||||
## 7. Data Accuracy Disclaimer
|
||||
|
||||
The data provided by leeworks.dev APIs is sourced from public datasets. We make no warranty as to the accuracy, completeness, or fitness for any particular purpose. You use the data at your own risk.
|
||||
|
||||
## 8. Service Availability
|
||||
|
||||
- We target 99.9% uptime but make no formal SLA guarantee on free or Basic plans.
|
||||
- We reserve the right to take the service down for maintenance with or without notice.
|
||||
- See `status.leeworks.dev` for real-time uptime information.
|
||||
|
||||
## 9. Intellectual Property
|
||||
|
||||
- The APIs, documentation, and underlying software are the intellectual property of leeworks.dev.
|
||||
- Response data may be used in your own products subject to these Terms.
|
||||
- You may not claim ownership of the data or present it as proprietary to you.
|
||||
|
||||
## 10. Termination
|
||||
|
||||
We may suspend or terminate your access to the Services immediately, without prior notice, for:
|
||||
- Violation of these Terms
|
||||
- Suspected abuse or fraud
|
||||
- Non-payment of applicable fees
|
||||
|
||||
Upon termination, your license to use the Services ceases immediately.
|
||||
|
||||
## 11. Limitation of Liability
|
||||
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LEEWORKS.DEV SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING LOSS OF PROFITS, DATA, OR BUSINESS, ARISING OUT OF OR IN CONNECTION WITH YOUR USE OF THE SERVICES.
|
||||
|
||||
## 12. Indemnification
|
||||
|
||||
You agree to indemnify and hold harmless leeworks.dev from any claims, damages, or expenses (including legal fees) arising from your use of the Services or violation of these Terms.
|
||||
|
||||
## 13. Changes to Terms
|
||||
|
||||
We may modify these Terms at any time. We will post changes on this page with an updated effective date. Continued use of the Services after changes constitutes acceptance.
|
||||
|
||||
## 14. Governing Law
|
||||
|
||||
These Terms are governed by the laws of the United States. Any disputes shall be resolved in the courts of appropriate jurisdiction.
|
||||
|
||||
## 15. Contact
|
||||
|
||||
Questions about these Terms? Contact us at: **legal@leeworks.dev**
|
||||
@@ -1,309 +0,0 @@
|
||||
# API Metrics Instrumentation Standard
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** 2026-05-24
|
||||
**Applies to:** All leeworks.dev API services (zip-enrichment, holidays, air-quality)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Every API service MUST expose Prometheus-compatible metrics at `GET /metrics`. This document defines the required metrics, label conventions, and provides reference middleware implementations for both Fastify (Node.js) and FastAPI (Python).
|
||||
|
||||
---
|
||||
|
||||
## Required Metrics
|
||||
|
||||
### 1. `api_requests_total`
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Type** | Counter |
|
||||
| **Description** | Total number of HTTP requests received |
|
||||
| **Labels** | `api`, `route`, `method`, `status` |
|
||||
|
||||
**Label values:**
|
||||
- `api`: one of `zip-enrichment`, `holidays`, `air-quality`
|
||||
- `route`: the matched route pattern, e.g. `/v1/lookup`, `/v1/holidays`
|
||||
- `method`: HTTP method, e.g. `GET`, `POST`
|
||||
- `status`: HTTP status code as string, e.g. `200`, `404`, `429`, `403`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="200"} 1234
|
||||
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="429"} 12
|
||||
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="403"} 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `api_response_duration_seconds`
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Type** | Histogram |
|
||||
| **Description** | HTTP response latency in seconds |
|
||||
| **Labels** | `api`, `route` |
|
||||
| **Buckets** | `0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5` |
|
||||
|
||||
**Example:**
|
||||
```
|
||||
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.05"} 800
|
||||
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.1"} 990
|
||||
api_response_duration_seconds_sum{api="holidays",route="/v1/holidays"} 45.2
|
||||
api_response_duration_seconds_count{api="holidays",route="/v1/holidays"} 1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `api_data_freshness_seconds`
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Type** | Gauge |
|
||||
| **Description** | Seconds since the local dataset was last seeded/refreshed |
|
||||
| **Labels** | `api`, `dataset` |
|
||||
| **Unit** | Seconds (Unix timestamp diff: `now - last_seed_time`) |
|
||||
|
||||
**Label values:**
|
||||
- `dataset`: a descriptive name for the dataset, e.g. `zip_codes`, `us_holidays`, `aqi_readings`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
api_data_freshness_seconds{api="air-quality",dataset="aqi_readings"} 86400
|
||||
api_data_freshness_seconds{api="zip-enrichment",dataset="zip_codes"} 2592000
|
||||
```
|
||||
|
||||
A value of `0` means freshly seeded; values growing toward `2592000` (30 days) are expected for monthly re-seed schedules.
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
### Fastify (Node.js/TypeScript)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
npm install prom-client
|
||||
```
|
||||
|
||||
**`src/metrics.ts`:**
|
||||
```typescript
|
||||
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
|
||||
|
||||
export const register = new Registry();
|
||||
|
||||
export const requestsTotal = new Counter({
|
||||
name: 'api_requests_total',
|
||||
help: 'Total number of HTTP requests received',
|
||||
labelNames: ['api', 'route', 'method', 'status'],
|
||||
registers: [register],
|
||||
});
|
||||
|
||||
export const responseDuration = new Histogram({
|
||||
name: 'api_response_duration_seconds',
|
||||
help: 'HTTP response latency in seconds',
|
||||
labelNames: ['api', 'route'],
|
||||
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
||||
registers: [register],
|
||||
});
|
||||
|
||||
export const dataFreshness = new Gauge({
|
||||
name: 'api_data_freshness_seconds',
|
||||
help: 'Seconds since the local dataset was last seeded',
|
||||
labelNames: ['api', 'dataset'],
|
||||
registers: [register],
|
||||
});
|
||||
```
|
||||
|
||||
**`src/metricsMiddleware.ts`:**
|
||||
```typescript
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { register, requestsTotal, responseDuration } from './metrics';
|
||||
|
||||
const API_NAME = process.env.API_NAME ?? 'unknown'; // set per service
|
||||
|
||||
export const metricsPlugin: FastifyPluginAsync = async (fastify) => {
|
||||
// Expose /metrics endpoint
|
||||
fastify.get('/metrics', async (_req, reply) => {
|
||||
reply.header('Content-Type', register.contentType);
|
||||
return register.metrics();
|
||||
});
|
||||
|
||||
// Instrument all routes
|
||||
fastify.addHook('onRequest', async (request, _reply) => {
|
||||
(request as any)._startTime = process.hrtime.bigint();
|
||||
});
|
||||
|
||||
fastify.addHook('onResponse', async (request, reply) => {
|
||||
const startTime = (request as any)._startTime as bigint;
|
||||
const durationMs = Number(process.hrtime.bigint() - startTime) / 1e6;
|
||||
const route = request.routerPath ?? request.url;
|
||||
|
||||
requestsTotal.labels(API_NAME, route, request.method, String(reply.statusCode)).inc();
|
||||
responseDuration.labels(API_NAME, route).observe(durationMs / 1000);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Register in main:**
|
||||
```typescript
|
||||
import { metricsPlugin } from './metricsMiddleware';
|
||||
await fastify.register(metricsPlugin);
|
||||
```
|
||||
|
||||
**Update data freshness gauge (call after each seed):**
|
||||
```typescript
|
||||
import { dataFreshness } from './metrics';
|
||||
// Call this after each DB seed completes:
|
||||
dataFreshness.labels('zip-enrichment', 'zip_codes').set(0);
|
||||
// Or set it to seconds since last seed on startup:
|
||||
dataFreshness.labels('zip-enrichment', 'zip_codes').set(secondsSinceLastSeed);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### FastAPI (Python)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
pip install prometheus-client starlette
|
||||
```
|
||||
|
||||
**`metrics.py`:**
|
||||
```python
|
||||
from prometheus_client import Counter, Histogram, Gauge, REGISTRY, CollectorRegistry
|
||||
|
||||
registry = CollectorRegistry()
|
||||
|
||||
requests_total = Counter(
|
||||
'api_requests_total',
|
||||
'Total number of HTTP requests received',
|
||||
['api', 'route', 'method', 'status'],
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
response_duration = Histogram(
|
||||
'api_response_duration_seconds',
|
||||
'HTTP response latency in seconds',
|
||||
['api', 'route'],
|
||||
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
data_freshness = Gauge(
|
||||
'api_data_freshness_seconds',
|
||||
'Seconds since the local dataset was last seeded',
|
||||
['api', 'dataset'],
|
||||
registry=registry,
|
||||
)
|
||||
```
|
||||
|
||||
**`metrics_middleware.py`:**
|
||||
```python
|
||||
import time
|
||||
import os
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
||||
from metrics import requests_total, response_duration, registry
|
||||
|
||||
API_NAME = os.getenv("API_NAME", "unknown")
|
||||
|
||||
|
||||
class MetricsMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
start = time.time()
|
||||
response = await call_next(request)
|
||||
duration = time.time() - start
|
||||
|
||||
route = request.url.path
|
||||
requests_total.labels(
|
||||
api=API_NAME,
|
||||
route=route,
|
||||
method=request.method,
|
||||
status=str(response.status_code),
|
||||
).inc()
|
||||
response_duration.labels(api=API_NAME, route=route).observe(duration)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def metrics_endpoint(request: Request):
|
||||
return Response(
|
||||
generate_latest(registry),
|
||||
media_type=CONTENT_TYPE_LATEST,
|
||||
)
|
||||
```
|
||||
|
||||
**Register in FastAPI app:**
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from starlette.routing import Route
|
||||
from metrics_middleware import MetricsMiddleware, metrics_endpoint
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(MetricsMiddleware)
|
||||
app.add_route("/metrics", metrics_endpoint)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prometheus Scrape Configuration
|
||||
|
||||
Add to your Prometheus `scrape_configs` (or ServiceMonitor for kube-prometheus-stack):
|
||||
|
||||
```yaml
|
||||
# prometheus-additional-scrapes.yaml
|
||||
- job_name: 'leeworks-apis'
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
|
||||
action: keep
|
||||
regex: "true"
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
|
||||
action: replace
|
||||
target_label: __metrics_path__
|
||||
regex: (.+)
|
||||
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
|
||||
action: replace
|
||||
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||
replacement: $1:$2
|
||||
target_label: __address__
|
||||
```
|
||||
|
||||
Add annotations to each API pod:
|
||||
```yaml
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "3000" # or 8000 for FastAPI
|
||||
prometheus.io/path: "/metrics"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grafana Dashboard
|
||||
|
||||
A reference dashboard JSON is available at `docs/grafana-api-dashboard.json` (TBD — will be committed once Grafana is deployed per issue #7).
|
||||
|
||||
Key panels to include:
|
||||
1. Request rate by API and status (`rate(api_requests_total[5m])`)
|
||||
2. P50/P95/P99 latency (`histogram_quantile(0.99, rate(api_response_duration_seconds_bucket[5m]))`)
|
||||
3. Error rate = non-2xx / total requests
|
||||
4. Data freshness gauge per API
|
||||
5. Request volume heatmap
|
||||
|
||||
---
|
||||
|
||||
## Compliance Checklist
|
||||
|
||||
Before marking an API server PR as ready:
|
||||
|
||||
- [ ] `GET /metrics` returns `text/plain; version=0.0.4; charset=utf-8`
|
||||
- [ ] `api_requests_total` increments on every request with correct labels
|
||||
- [ ] `api_response_duration_seconds` has observations on every request
|
||||
- [ ] `api_data_freshness_seconds` is set on startup and after each seed
|
||||
- [ ] Pod annotations for Prometheus scraping are present in the Helm chart values
|
||||
- [ ] `API_NAME` env var is set correctly per deployment
|
||||
@@ -1,309 +0,0 @@
|
||||
# Operator Runbook
|
||||
|
||||
**Audience:** Human operator (0xWheatyz)
|
||||
**Purpose:** Ordered, copy-paste-ready guide to bring the full `api-company` stack live.
|
||||
**Last updated:** 2026-05-26
|
||||
**Closes:** leeworks-agents/api-company#50
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The agent has committed all Flux manifests and documentation. The only remaining
|
||||
work is a set of manual steps that require Gitea admin access, `kubectl` access to
|
||||
the `testing1` cluster, and external service accounts (RapidAPI, Slack, PayPal).
|
||||
|
||||
Work through these phases **in order** — each phase unblocks the next.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Create upstream repo (unblocks all ship cycles)
|
||||
|
||||
> **Why first?** Every agent deployment cycle fails to open a PR to upstream
|
||||
> because `0xWheatyz/api-company` does not yet exist. This one step unblocks
|
||||
> all automated deployments. See issues #41, #47.
|
||||
|
||||
### Step 0-A — Create `0xWheatyz/api-company` on Gitea
|
||||
|
||||
1. Log into Gitea as `0xWheatyz`.
|
||||
2. **+** → **New Repository**.
|
||||
3. Owner: `0xWheatyz`, Name: `api-company`.
|
||||
4. Visibility: Public (or Private — your choice).
|
||||
5. **Do not** initialise with a README.
|
||||
6. Click **Create Repository**.
|
||||
|
||||
Once created, the agent's next ship cycle will open a deployment PR automatically.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Wire Flux to this repo (unblocks all GitOps reconciliation)
|
||||
|
||||
> **Why second?** Until Flux watches `leeworks-agents/api-company`, none of the
|
||||
> manifests in `flux/` are applied to the cluster. See issue #2.
|
||||
|
||||
### Step 1-A — Create `gitea-leeworks-agents-token` secret in `flux-system`
|
||||
|
||||
```bash
|
||||
# In Gitea: User Settings → Applications → Generate Token
|
||||
# Scopes: read:repository (read-only is sufficient for Flux)
|
||||
# Copy the token, then:
|
||||
|
||||
kubectl create secret generic gitea-leeworks-agents-token \
|
||||
-n flux-system \
|
||||
--from-literal=username=leeworks-agents \
|
||||
--from-literal=password=<GITEA_TOKEN>
|
||||
```
|
||||
|
||||
### Step 1-B — Copy Flux source + kustomization into 0xWheatyz/Talos
|
||||
|
||||
Reference manifests are at `flux/api-company-source/` in this repo.
|
||||
Copy them verbatim to:
|
||||
|
||||
```
|
||||
0xWheatyz/Talos:testing1/first-cluster/cluster/flux/api-company-source/
|
||||
├── gitrepository.yaml
|
||||
└── kustomization.yaml
|
||||
```
|
||||
|
||||
You can do this via the Gitea web editor or locally:
|
||||
|
||||
```bash
|
||||
cd /path/to/Talos-checkout
|
||||
mkdir -p testing1/first-cluster/cluster/flux/api-company-source
|
||||
# copy the two files from api-company/flux/api-company-source/
|
||||
git add .
|
||||
git commit -m "feat: wire Flux GitRepository + Kustomization for api-company"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**Verify reconciliation (after ~5 minutes):**
|
||||
```bash
|
||||
flux get sources git -n flux-system
|
||||
flux get kustomizations -n flux-system
|
||||
```
|
||||
|
||||
Both `api-company` entries should show `Ready = True`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Secrets for already-staged services
|
||||
|
||||
Once Flux is watching the repo, it will attempt to reconcile all `flux/`
|
||||
sub-directories. The HelmReleases will stall on missing secrets. Create them:
|
||||
|
||||
### Step 2-A — `gitea-runner-token` (unblocks Gitea Actions runner, issue #3)
|
||||
|
||||
```bash
|
||||
# In Gitea: Admin Panel → Site Administration → Actions → Runners
|
||||
# → Create new Runner → copy registration token
|
||||
|
||||
kubectl create secret generic gitea-runner-token \
|
||||
-n gitea-runner \
|
||||
--from-literal=token=<RUNNER_REGISTRATION_TOKEN>
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
kubectl get pods -n gitea-runner
|
||||
# Then check Gitea Admin → Actions → Runners — runner should appear Online
|
||||
```
|
||||
|
||||
### Step 2-B — `grafana-admin` (unblocks Grafana, issue #7)
|
||||
|
||||
```bash
|
||||
kubectl create secret generic grafana-admin \
|
||||
-n monitoring \
|
||||
--from-literal=admin-password=<CHOOSE_STRONG_PASSWORD>
|
||||
```
|
||||
|
||||
Grafana URL: `https://grafana.leeworks.dev` (login: `admin` / `<PASSWORD>`)
|
||||
|
||||
### Step 2-C — `gatus-slack-webhook` (unblocks Gatus alerts, issue #8)
|
||||
|
||||
```bash
|
||||
# Create an incoming webhook at: https://api.slack.com/messaging/webhooks
|
||||
|
||||
kubectl create secret generic gatus-slack-webhook \
|
||||
-n monitoring \
|
||||
--from-literal=url=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
```
|
||||
|
||||
Gatus URL: `https://status.leeworks.dev`
|
||||
|
||||
### Step 2-D — Enable Gitea packages + registry DNS (issue #4)
|
||||
|
||||
**4a — Enable packages in Gitea `app.ini`:**
|
||||
```ini
|
||||
[packages]
|
||||
ENABLED = true
|
||||
```
|
||||
Restart Gitea after editing `app.ini`.
|
||||
|
||||
**4b — Add DNS A record:**
|
||||
```
|
||||
registry.leeworks.dev → <cluster ingress IP>
|
||||
```
|
||||
|
||||
Find the ingress IP:
|
||||
```bash
|
||||
kubectl get svc -n ingress-nginx
|
||||
```
|
||||
|
||||
See `docs/registry.md` for additional context.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Enable CI image push (unblocks API service deployments)
|
||||
|
||||
Once the runner is online and the registry is reachable, CI pipelines can build
|
||||
and push container images.
|
||||
|
||||
### Step 3-A — Add `GITEA_TOKEN` Actions Secret to each repo
|
||||
|
||||
Repos to configure:
|
||||
- `leeworks-agents/api-company`
|
||||
- `leeworks-agents/zip-enrichment`
|
||||
- `leeworks-agents/holidays`
|
||||
- `leeworks-agents/air-quality`
|
||||
|
||||
**For each repo:** Repo → Settings → Actions → Secrets → Add Secret
|
||||
- **Name:** `GITEA_TOKEN`
|
||||
- **Value:** Gitea personal access token with `write:packages` scope
|
||||
|
||||
### Step 3-B — Create image-automation token secret (issue #51)
|
||||
|
||||
The agent has added `ImageRepository` + `ImagePolicy` + `ImageUpdateAutomation`
|
||||
manifests to `flux/image-automation/`. Flux will automatically update image tags
|
||||
in HelmReleases when CI pushes new images — but it needs write access to commit
|
||||
back:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gitea-image-automation-token \
|
||||
-n flux-system \
|
||||
--from-literal=username=leeworks-agents \
|
||||
--from-literal=password=<GITEA_TOKEN_WITH_WRITE_REPO>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — DNS for API services (issue #33)
|
||||
|
||||
Add DNS A records for all seven leeworks.dev subdomains (all point to the same
|
||||
cluster ingress IP):
|
||||
|
||||
| Hostname | Target |
|
||||
|-------------------------|------------------------|
|
||||
| `zip.leeworks.dev` | `<cluster ingress IP>` |
|
||||
| `holidays.leeworks.dev` | `<cluster ingress IP>` |
|
||||
| `aqi.leeworks.dev` | `<cluster ingress IP>` |
|
||||
| `docs.leeworks.dev` | `<cluster ingress IP>` |
|
||||
| `grafana.leeworks.dev` | `<cluster ingress IP>` |
|
||||
| `status.leeworks.dev` | `<cluster ingress IP>` |
|
||||
| `registry.leeworks.dev` | `<cluster ingress IP>` |
|
||||
|
||||
Verify DNS propagation:
|
||||
|
||||
```bash
|
||||
for host in zip holidays aqi docs grafana status registry; do
|
||||
echo -n "${host}.leeworks.dev: "
|
||||
dig ${host}.leeworks.dev +short
|
||||
done
|
||||
```
|
||||
|
||||
cert-manager will obtain Let's Encrypt certificates automatically once DNS
|
||||
propagates (typically minutes, up to 48 h).
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — RapidAPI + PayPal (issue #44, #19)
|
||||
|
||||
> **Blocked on operator being 18+ for PayPal.** Complete when eligible.
|
||||
|
||||
1. Create accounts on [rapidapi.com](https://rapidapi.com) and [paypal.com](https://www.paypal.com).
|
||||
2. Link PayPal to RapidAPI as the payout method.
|
||||
3. Submit each API to the RapidAPI marketplace using `docs/rapidapi-listings.md`.
|
||||
4. Configure paid tiers per `ROADMAP.md`.
|
||||
|
||||
After submission, RapidAPI generates a `X-RapidAPI-Proxy-Secret` per API. Create:
|
||||
|
||||
```bash
|
||||
# zip-enrichment
|
||||
kubectl create secret generic rapidapi-proxy-secret \
|
||||
-n zip-enrichment \
|
||||
--from-literal=X-RapidAPI-Proxy-Secret=<VALUE>
|
||||
|
||||
# holidays
|
||||
kubectl create secret generic rapidapi-proxy-secret \
|
||||
-n holidays \
|
||||
--from-literal=X-RapidAPI-Proxy-Secret=<VALUE>
|
||||
|
||||
# air-quality
|
||||
kubectl create secret generic rapidapi-proxy-secret \
|
||||
-n air-quality \
|
||||
--from-literal=X-RapidAPI-Proxy-Secret=<VALUE>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Verification Checklist
|
||||
|
||||
```bash
|
||||
# Flux overall health
|
||||
flux get all -A
|
||||
|
||||
# API service pods
|
||||
kubectl get pods -n zip-enrichment
|
||||
kubectl get pods -n holidays
|
||||
kubectl get pods -n air-quality
|
||||
|
||||
# Ingress + TLS
|
||||
kubectl get ingress -A
|
||||
kubectl get certificates -A
|
||||
|
||||
# Gitea runner
|
||||
kubectl get pods -n gitea-runner
|
||||
|
||||
# Monitoring stack
|
||||
kubectl get pods -n monitoring
|
||||
|
||||
# Image automation
|
||||
flux get imagepolicies -A
|
||||
flux get imagerepositories -A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Summary
|
||||
|
||||
```
|
||||
Phase 0: Create 0xWheatyz/api-company repo
|
||||
└─► unblocks agent deployment PRs to upstream
|
||||
|
||||
Phase 1: Wire Flux (gitea-token secret + Talos manifests)
|
||||
└─► all flux/ manifests reconcile
|
||||
|
||||
Phase 2: Service secrets (runner-token, grafana-admin, gatus-webhook, registry)
|
||||
└─► runner online, monitoring live, registry reachable
|
||||
|
||||
Phase 3: CI secrets + image-automation token
|
||||
└─► images build, push, and auto-update → API services deploy
|
||||
|
||||
Phase 4: DNS records
|
||||
└─► HTTPS certs issued → public URLs go live
|
||||
|
||||
Phase 5: RapidAPI + PayPal
|
||||
└─► revenue enabled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| `docs/secrets-checklist.md` | Full checklist of all required secrets |
|
||||
| `docs/registry.md` | Container registry architecture decision |
|
||||
| `docs/cluster-audit.md` | Node/namespace/ingress inventory |
|
||||
| `docs/rapidapi-listings.md` | RapidAPI marketplace submission details |
|
||||
| `ROADMAP.md` | Full project roadmap and milestones |
|
||||
| `STATUS.md` | Current cycle status and blockers |
|
||||
@@ -1,105 +0,0 @@
|
||||
# Pre-Launch Checklist
|
||||
|
||||
Use this checklist as the final go-live gate — run through every item the day before flipping the public switch. All items must be ✅ before announcing public availability.
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- [ ] All Flux components `READY=True` (`flux get all`)
|
||||
- [ ] All three API pods Running and READY
|
||||
- [ ] `zip-enrichment` pod `READY=1/1` (`kubectl get pods -n zip-enrichment`)
|
||||
- [ ] `holidays` pod `READY=1/1` (`kubectl get pods -n holidays`)
|
||||
- [ ] `air-quality` pod `READY=1/1` (`kubectl get pods -n air-quality`)
|
||||
- [ ] `vin-decoder` pod `READY=1/1` (`kubectl get pods -n vin-decoder`)
|
||||
- [ ] `docs-site` pod Running and READY (`kubectl get pods -n docs-site`)
|
||||
- [ ] Prometheus scraping all three API services (check Prometheus Targets UI)
|
||||
- [ ] Grafana dashboard accessible at `grafana.leeworks.dev`
|
||||
|
||||
---
|
||||
|
||||
## DNS & TLS
|
||||
|
||||
- [ ] `zip.leeworks.dev` → cluster ingress IP (`dig zip.leeworks.dev +short` + `curl -I https://zip.leeworks.dev`)
|
||||
- [ ] `holidays.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `aqi.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `docs.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `status.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `registry.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `vin.leeworks.dev` → cluster ingress IP (`dig vin.leeworks.dev +short` + `curl -I https://vin.leeworks.dev`)
|
||||
- [ ] `grafana.leeworks.dev` → cluster ingress IP
|
||||
- [ ] TLS certificates issued for all 8 subdomains (`kubectl get certificates -A`)
|
||||
|
||||
---
|
||||
|
||||
## Functional Verification
|
||||
|
||||
- [ ] `GET /health` returns HTTP 200 on **zip-enrichment** (`curl https://zip.leeworks.dev/health`)
|
||||
- [ ] `GET /health` returns HTTP 200 on **holidays** (`curl https://holidays.leeworks.dev/health`)
|
||||
- [ ] `GET /health` returns HTTP 200 on **air-quality** (`curl https://aqi.leeworks.dev/health`)
|
||||
- [ ] `GET /zip/{zip}` returns correct data for a sample ZIP code (e.g. `curl https://zip.leeworks.dev/zip/10001`)
|
||||
- [ ] `GET /holidays/{year}` returns correct data (e.g. `curl https://holidays.leeworks.dev/holidays/2026`)
|
||||
- [ ] `GET /aqi/{city}` returns correct data (e.g. `curl https://aqi.leeworks.dev/aqi/New%20York`)
|
||||
- [ ] `GET /v1/health` returns HTTP 200 on **vin-decoder** (`curl https://vin.leeworks.dev/v1/health`)
|
||||
- [ ] `GET /v1/decode?vin=1HGCM82633A004352` returns correct make/model/year data
|
||||
- [ ] Request **without** `X-RapidAPI-Proxy-Secret` returns HTTP 403 on **vin-decoder** (`curl https://vin.leeworks.dev/v1/decode?vin=1HGCM82633A004352`)
|
||||
- [ ] Request **without** `X-RapidAPI-Proxy-Secret` returns HTTP 403 on all three APIs
|
||||
- [ ] `docs.leeworks.dev/pricing` loads correctly
|
||||
- [ ] `status.leeworks.dev` shows all **four** APIs as **UP**
|
||||
|
||||
---
|
||||
|
||||
## Legal & Monetisation
|
||||
|
||||
- [ ] `docs/legal/terms-of-service.md` committed and reachable at `docs.leeworks.dev/legal/terms-of-service`
|
||||
- [ ] `docs/legal/privacy-policy.md` committed and reachable at `docs.leeworks.dev/legal/privacy-policy`
|
||||
- [ ] `docs/legal/acceptable-use-policy.md` committed and reachable at `docs.leeworks.dev/legal/acceptable-use-policy`
|
||||
- [ ] All three APIs listed on RapidAPI with **Free + 3 paid tiers** (leeworks-agents/api-company#44)
|
||||
- [ ] VIN Decoder listed on RapidAPI with Free + 3 paid tiers (leeworks-agents/api-company#131)
|
||||
- [ ] `rapidapi-proxy-secret` updated with real RapidAPI value in `vin-decoder` namespace (leeworks-agents/api-company#128)
|
||||
- [ ] PayPal linked to RapidAPI (leeworks-agents/api-company#19)
|
||||
- [ ] `rapidapi-proxy-secret` updated with **real** RapidAPI values in all 3 namespaces (leeworks-agents/api-company#81)
|
||||
|
||||
---
|
||||
|
||||
## Post-Launch
|
||||
|
||||
- [ ] Alert channel confirmed — Slack webhook active (leeworks-agents/api-company#73)
|
||||
- [ ] Grafana alert rules firing correctly on synthetic test downtime
|
||||
- [ ] First STATUS.md post-launch update committed
|
||||
|
||||
---
|
||||
|
||||
## Quick Commands Reference
|
||||
|
||||
```bash
|
||||
# Flux overview
|
||||
flux get all
|
||||
|
||||
# Check API pod health
|
||||
for ns in zip-enrichment holidays air-quality docs-site; do
|
||||
echo "=== $ns ==="
|
||||
kubectl get pods -n $ns
|
||||
done
|
||||
|
||||
# Verify TLS certs
|
||||
kubectl get certificates -A
|
||||
|
||||
# Check all 7 DNS subdomains
|
||||
for sub in zip holidays aqi docs status registry grafana; do
|
||||
echo "$sub.leeworks.dev -> $(dig $sub.leeworks.dev +short)"
|
||||
done
|
||||
|
||||
# Smoke test health endpoints
|
||||
for svc in "zip.leeworks.dev/health" "holidays.leeworks.dev/health" "aqi.leeworks.dev/health"; do
|
||||
echo "$svc: $(curl -s -o /dev/null -w '%{http_code}' https://$svc)"
|
||||
done
|
||||
|
||||
# Verify 403 on missing proxy secret
|
||||
curl -s -o /dev/null -w "%{http_code}" https://zip.leeworks.dev/zip/10001
|
||||
# Expected: 403
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_References: ROADMAP.md §Phase 6; leeworks-agents/api-company#33, #44, #19, #81, #73, #7, #8, #18_
|
||||
@@ -1,288 +0,0 @@
|
||||
# RapidAPI Marketplace Listings
|
||||
|
||||
Copy-ready listing content for the three leeworks.dev APIs.
|
||||
Paste into the RapidAPI dashboard when paid tiers are enabled (see issue #19).
|
||||
|
||||
---
|
||||
|
||||
## 1. ZIP Code Enrichment API
|
||||
|
||||
### API Name
|
||||
ZIP Code Enrichment API
|
||||
|
||||
### Tagline
|
||||
Instantly look up city, state, county, timezone, and coordinates for any US ZIP code.
|
||||
|
||||
### Short Description (≤ 300 chars)
|
||||
Turn any US ZIP code into rich location data: city, state, county, timezone offset, area codes, and GPS coordinates. Single-lookup and bulk-batch endpoints. Powered by a monthly-refreshed dataset covering 43,000+ ZIP codes.
|
||||
|
||||
### Long Description
|
||||
|
||||
Transform raw ZIP codes into actionable location intelligence with a single API call.
|
||||
|
||||
**What you get per lookup:**
|
||||
- City name and state (abbreviation + full name)
|
||||
- County name and FIPS code
|
||||
- Timezone (IANA name + UTC offset)
|
||||
- Area codes (may be multiple)
|
||||
- Latitude / longitude (centroid)
|
||||
- ZIP classification (PO Box, standard, military, unique)
|
||||
|
||||
**Data freshness:** Dataset is re-seeded from USPS/US Census public data on the 1st of each month. The `api_data_freshness_seconds` metric is exposed on `/metrics` for real-time freshness monitoring.
|
||||
|
||||
**Rate limits:** See plan table below. All plans share the same endpoints; higher plans unlock more requests per month and per second.
|
||||
|
||||
**Use cases:**
|
||||
- Address auto-complete & validation in checkout flows
|
||||
- Route-planning and delivery-zone calculations
|
||||
- CRM enrichment for sales-territory assignment
|
||||
- Fraud detection (ZIP-to-carrier mismatch checks)
|
||||
- Census / analytics workflows
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /v1/lookup` — look up a single ZIP code
|
||||
- `POST /v1/bulk` — look up up to 100 ZIP codes in one request
|
||||
- `GET /health` — service health check
|
||||
- `GET /metrics` — Prometheus metrics endpoint (internal)
|
||||
|
||||
### Category
|
||||
Data / Location
|
||||
|
||||
### Plan Table
|
||||
|
||||
| Plan | Price/month | Requests/month | Rate limit |
|
||||
|------|-------------|----------------|------------|
|
||||
| Basic | $9 | 10,000 | 5 req/sec |
|
||||
| Pro | $19 | 50,000 | 20 req/sec |
|
||||
| Ultra | $49 | 250,000 | 60 req/sec |
|
||||
|
||||
### Endpoint Descriptions
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /v1/lookup?zip={zip}` | Returns city, state, county, timezone, area codes, and coordinates for the given 5-digit US ZIP code. |
|
||||
| `POST /v1/bulk` | Accepts an array of up to 100 ZIP codes and returns enrichment data for each. |
|
||||
| `GET /health` | Returns `{"status":"ok"}` when the service is healthy. |
|
||||
|
||||
### Keywords
|
||||
zip code, postal code, address enrichment, US location, geocoding, city state lookup, timezone, county, FIPS, address validation
|
||||
|
||||
---
|
||||
|
||||
## 2. Public Holidays API
|
||||
|
||||
### API Name
|
||||
Public Holidays API
|
||||
|
||||
### Tagline
|
||||
Query official public holidays for any country and year — reliable, cached, blazing fast.
|
||||
|
||||
### Short Description (≤ 300 chars)
|
||||
Access verified public holiday calendars for 100+ countries. Filter by year, country, or region. Ideal for scheduling apps, payroll systems, and calendar integrations. Monthly-refreshed dataset with ISO 8601 dates.
|
||||
|
||||
### Long Description
|
||||
|
||||
Power your scheduling, payroll, and calendar features with accurate public holiday data from around the world.
|
||||
|
||||
**Coverage:**
|
||||
- 100+ countries with ISO 3166-1 alpha-2 country codes
|
||||
- National and regional/state-level holidays where available
|
||||
- Holiday names in English (and native language where available)
|
||||
- Holiday type (public, bank, school, optional)
|
||||
- ISO 8601 dates for easy parsing in any language
|
||||
|
||||
**Data freshness:** Holiday data is sourced from official government publications and open-data registries, re-seeded monthly. Includes a full 5-year forward window for scheduling purposes.
|
||||
|
||||
**Use cases:**
|
||||
- Payroll systems that need to skip or flag holidays
|
||||
- Appointment-booking tools that grey-out non-working days
|
||||
- Shipping & logistics — SLA calculators that skip holidays
|
||||
- Finance apps — market closure calendars
|
||||
- HR software — leave management and working-days counters
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /v1/holidays` — list holidays for a country and year
|
||||
- `GET /v1/countries` — list all supported countries
|
||||
- `GET /health` — service health check
|
||||
- `GET /metrics` — Prometheus metrics endpoint (internal)
|
||||
|
||||
### Category
|
||||
Data / Finance / Calendar
|
||||
|
||||
### Plan Table
|
||||
|
||||
| Plan | Price/month | Requests/month | Rate limit |
|
||||
|------|-------------|----------------|------------|
|
||||
| Basic | $9 | 10,000 | 5 req/sec |
|
||||
| Pro | $19 | 50,000 | 20 req/sec |
|
||||
| Ultra | $49 | 250,000 | 60 req/sec |
|
||||
|
||||
### Endpoint Descriptions
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /v1/holidays?country={cc}&year={yyyy}` | Returns all public holidays for the specified ISO 3166-1 alpha-2 country code and 4-digit year. |
|
||||
| `GET /v1/countries` | Returns a list of all supported country codes and their display names. |
|
||||
| `GET /health` | Returns `{"status":"ok"}` when the service is healthy. |
|
||||
|
||||
### Keywords
|
||||
public holidays, bank holidays, national holidays, calendar API, working days, payroll, scheduling, country holidays, ISO 3166, business calendar
|
||||
|
||||
---
|
||||
|
||||
## 3. Air Quality API
|
||||
|
||||
### API Name
|
||||
Air Quality Index API
|
||||
|
||||
### Tagline
|
||||
Real-time and historical AQI data for thousands of monitoring stations worldwide.
|
||||
|
||||
### Short Description (≤ 300 chars)
|
||||
Query current and historical Air Quality Index (AQI) readings by city, coordinates, or station ID. Covers PM2.5, PM10, O3, NO2, SO2, CO pollutants. Data from government monitoring stations, refreshed monthly.
|
||||
|
||||
### Long Description
|
||||
|
||||
Integrate air quality intelligence into health apps, smart-home devices, travel planners, and environmental dashboards.
|
||||
|
||||
**Data coverage:**
|
||||
- AQI values (US EPA scale, 0–500+) and category (Good / Moderate / Unhealthy / etc.)
|
||||
- Individual pollutant concentrations: PM2.5, PM10, O₃, NO₂, SO₂, CO
|
||||
- Station metadata: name, city, country, latitude/longitude
|
||||
- Lookup by city name, geographic coordinates (lat/lon radius), or station ID
|
||||
- Historical readings window (monthly granularity)
|
||||
|
||||
**Data freshness:** Station readings are ingested from public government AQI registries and the OpenAQ dataset, re-seeded monthly. The `api_data_freshness_seconds` metric tracks time since last seed.
|
||||
|
||||
**Use cases:**
|
||||
- Fitness / outdoor activity apps — warn users when air quality is poor
|
||||
- Smart-home & IoT dashboards — display local AQI alongside temperature
|
||||
- Travel apps — highlight air quality concerns at destinations
|
||||
- Environmental research — pull historical AQI time-series data
|
||||
- Real-estate platforms — include air quality scores in neighborhood profiles
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /v1/aqi` — look up current AQI by city or coordinates
|
||||
- `GET /v1/stations` — list monitoring stations (filterable by country/city)
|
||||
- `GET /v1/history` — historical AQI readings for a station
|
||||
- `GET /health` — service health check
|
||||
- `GET /metrics` — Prometheus metrics endpoint (internal)
|
||||
|
||||
### Category
|
||||
Data / Weather / Environment
|
||||
|
||||
### Plan Table
|
||||
|
||||
| Plan | Price/month | Requests/month | Rate limit |
|
||||
|------|-------------|----------------|------------|
|
||||
| Basic | $9 | 10,000 | 5 req/sec |
|
||||
| Pro | $19 | 50,000 | 20 req/sec |
|
||||
| Ultra | $49 | 250,000 | 60 req/sec |
|
||||
|
||||
### Endpoint Descriptions
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /v1/aqi?city={city}` or `?lat={lat}&lon={lon}` | Returns the current AQI and individual pollutant readings for the nearest monitoring station to the requested location. |
|
||||
| `GET /v1/stations?country={cc}&city={city}` | Lists available AQI monitoring stations, optionally filtered by country (ISO 3166-1 alpha-2) and/or city name. |
|
||||
| `GET /v1/history?station={id}&year={yyyy}&month={mm}` | Returns historical monthly AQI readings for the specified station. |
|
||||
| `GET /health` | Returns `{"status":"ok"}` when the service is healthy. |
|
||||
|
||||
### Keywords
|
||||
air quality, AQI, PM2.5, PM10, air pollution, smog, ozone, nitrogen dioxide, environmental data, OpenAQ
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 4. VIN Decoder API
|
||||
|
||||
### API Name
|
||||
VIN Decoder API
|
||||
|
||||
### Tagline
|
||||
Decode any vehicle VIN into make, model, year, engine, and trim — powered by NHTSA vPIC.
|
||||
|
||||
### Short Description (≤ 300 chars)
|
||||
Decode any 17-character Vehicle Identification Number into structured vehicle data: make, model, year, trim, body style, engine specs, transmission, and assembly plant. Backed by the NHTSA vPIC public database with 90-day result caching.
|
||||
|
||||
### Long Description
|
||||
|
||||
Unlock the full story behind any Vehicle Identification Number with a single API call.
|
||||
|
||||
**What you get per VIN:**
|
||||
- Make, model, model year, and trim level
|
||||
- Body class (Sedan, SUV, Pickup, etc.) and drive type (FWD, RWD, AWD, 4WD)
|
||||
- Engine displacement (CC and litres) and cylinder count
|
||||
- Primary fuel type (Gasoline, Diesel, Electric, Hybrid, etc.)
|
||||
- Transmission style (Automatic, Manual, CVT) and speed count
|
||||
- Assembly plant city, state, and country
|
||||
- Full manufacturer name and NHTSA vehicle type classification
|
||||
- NHTSA decode error code and text for non-standard VINs
|
||||
|
||||
**Data source:** NHTSA Product Information Catalog and Vehicle Listing (vPIC) — US federal government public-domain data, always current. No licensing fees.
|
||||
|
||||
**Coverage:** Model years 1981–present. All major domestic and import manufacturers registered with NHTSA.
|
||||
|
||||
**Caching:** Decoded VINs are cached for 90 days in a local SQLite store. The `X-Cache: HIT/MISS` response header tells you whether the result came from cache or a live NHTSA lookup.
|
||||
|
||||
**Use cases:**
|
||||
- Used-car marketplaces — enrich listings with decoded specs at scale
|
||||
- Insurance platforms — auto-populate vehicle details from VIN at quote time
|
||||
- Fleet management — maintain structured vehicle inventories without manual entry
|
||||
- Automotive valuation tools — feed year/make/model/trim into pricing algorithms
|
||||
- Recall & warranty systems — match VINs to manufacturer service campaigns
|
||||
- Registration & titling apps — validate and enrich VIN data in DMV workflows
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /v1/decode` — decode a single VIN
|
||||
- `POST /v1/batch` — decode up to 50 VINs in one request
|
||||
- `GET /v1/health` — service health check (no auth required)
|
||||
|
||||
### Category
|
||||
Data / Automotive / Transportation
|
||||
|
||||
### Plan Table
|
||||
|
||||
| Tier | Price/month | Requests/month | Rate limit |
|
||||
|------|-------------|----------------|------------|
|
||||
| Free | $0 | 100 req/mo | 5 req/min |
|
||||
| Basic | $9 | 5,000 req/mo | 60 req/min |
|
||||
| Pro | $19 | 20,000 req/mo | 200 req/min |
|
||||
| Ultra | $49 | 100,000 req/mo | 500 req/min |
|
||||
|
||||
### Endpoint Descriptions
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /v1/decode?vin={vin}` | Decodes a single 17-character VIN. Returns structured vehicle attributes including make, model, year, engine, body, drivetrain, and plant info. Optional `?raw=true` includes the full NHTSA vPIC response. |
|
||||
| `POST /v1/batch` | Accepts a JSON body with a `vins` array (1–50 VINs). Returns one decoded result (or error) per VIN in input order, plus aggregate counts for `cached_count` and `error_count`. |
|
||||
| `GET /v1/health` | Returns service status, uptime, cache stats (entries, hit rate, size), and NHTSA upstream reachability. No `X-RapidAPI-Proxy-Secret` required. |
|
||||
|
||||
### Keywords
|
||||
VIN decoder, vehicle identification number, car lookup, NHTSA, make model year, automotive API, vehicle data, VIN lookup, auto specs, fleet management
|
||||
|
||||
---
|
||||
|
||||
## Tagline Length Validation
|
||||
|
||||
Run to confirm all taglines are ≤ 120 characters:
|
||||
|
||||
```bash
|
||||
awk '/^### Tagline/{getline; print length, $0}' docs/rapidapi-listings.md
|
||||
```
|
||||
|
||||
Expected output — all values < 120:
|
||||
```
|
||||
84 Instantly look up city, state, county, timezone, and coordinates for any US ZIP code.
|
||||
82 Query official public holidays for any country and year — reliable, cached, blazing fast.
|
||||
80 Real-time and historical AQI data for thousands of monitoring stations worldwide.
|
||||
```
|
||||
|
||||
## Short Description Length Validation
|
||||
|
||||
```bash
|
||||
awk '/^### Short Description/{getline; getline; print length, $0}' docs/rapidapi-listings.md
|
||||
```
|
||||
|
||||
All values should be ≤ 300 characters.
|
||||
@@ -1,163 +0,0 @@
|
||||
# Container Registry: registry.leeworks.dev
|
||||
|
||||
**Decision Date:** 2026-05-24
|
||||
**Status:** Planned (Phase 0 prerequisite)
|
||||
|
||||
---
|
||||
|
||||
## Decision: Use Gitea's Built-in Container Registry
|
||||
|
||||
We will use **Gitea's built-in container registry** (OCI-compatible, enabled via `GITEA_CONTAINER_REGISTRY`) rather than deploying a separate `distribution/distribution` instance.
|
||||
|
||||
### Rationale
|
||||
|
||||
1. **No new infra** — Gitea is already deployed; enabling the container registry is a config flag, not a new deployment.
|
||||
2. **Integrated auth** — API keys, org-scoped tokens, and CI secrets work natively with the Gitea registry.
|
||||
3. **Simpler CI** — Gitea Actions workflows can use `${{ secrets.GITEA_TOKEN }}` to push to `gitea.leeworks.dev/leeworks-agents/<image>`.
|
||||
4. **OCI compliance** — Gitea's container registry is OCI v1 compliant, compatible with Docker, Podman, and Kubernetes image pulls.
|
||||
|
||||
---
|
||||
|
||||
## Registry Hostname
|
||||
|
||||
```
|
||||
registry.leeworks.dev
|
||||
```
|
||||
|
||||
This will be a reverse proxy/ingress alias for `gitea.leeworks.dev` (Gitea's container registry endpoint).
|
||||
|
||||
Alternatively, Docker clients can use the Gitea hostname directly:
|
||||
```
|
||||
gitea.leeworks.dev/leeworks-agents/<image>:<tag>
|
||||
```
|
||||
|
||||
If a separate hostname is preferred by the operator, configure an Nginx ingress to proxy `registry.leeworks.dev` → Gitea's container registry port.
|
||||
|
||||
---
|
||||
|
||||
## Image Naming Convention
|
||||
|
||||
```
|
||||
registry.leeworks.dev/leeworks-agents/<repo-name>:<tag>
|
||||
```
|
||||
|
||||
| API | Image |
|
||||
|-----|-------|
|
||||
| ZIP Enrichment | `registry.leeworks.dev/leeworks-agents/zip-enrichment:latest` |
|
||||
| Holidays | `registry.leeworks.dev/leeworks-agents/holidays:latest` |
|
||||
| Air Quality | `registry.leeworks.dev/leeworks-agents/air-quality:latest` |
|
||||
| Docs Site | `registry.leeworks.dev/leeworks-agents/docs-site:latest` |
|
||||
|
||||
Tags should also include the git SHA for traceability: `:<sha>` in addition to `:latest`.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### Pushing from CI (Gitea Actions)
|
||||
|
||||
```yaml
|
||||
- name: Log in to registry
|
||||
run: |
|
||||
echo "${{ secrets.GITEA_TOKEN }}" | docker login registry.leeworks.dev \
|
||||
-u ${{ gitea.actor }} --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
docker build -t registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }} .
|
||||
docker push registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }}
|
||||
docker tag registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }} \
|
||||
registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:latest
|
||||
docker push registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:latest
|
||||
```
|
||||
|
||||
### Pulling from Kubernetes
|
||||
|
||||
Create an image pull secret in each namespace:
|
||||
|
||||
```bash
|
||||
kubectl create secret docker-registry gitea-registry \
|
||||
--docker-server=registry.leeworks.dev \
|
||||
--docker-username=<gitea-user> \
|
||||
--docker-password=<gitea-token> \
|
||||
--docker-email=ci@leeworks.dev \
|
||||
-n <namespace>
|
||||
```
|
||||
|
||||
Reference in pod spec:
|
||||
```yaml
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enabling Gitea Container Registry
|
||||
|
||||
If not already enabled, the Gitea administrator needs to ensure:
|
||||
|
||||
1. In `app.ini` (or Helm values), container registry is enabled:
|
||||
```ini
|
||||
[packages]
|
||||
ENABLED = true
|
||||
```
|
||||
2. The Gitea service is accessible on port 443 at `gitea.leeworks.dev`.
|
||||
3. If using `registry.leeworks.dev` as an alias, configure an Nginx Ingress:
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: registry-ingress
|
||||
namespace: gitea
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- registry.leeworks.dev
|
||||
secretName: registry-tls
|
||||
rules:
|
||||
- host: registry.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gitea-http
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Test login
|
||||
docker login registry.leeworks.dev -u <user> -p <token>
|
||||
|
||||
# Test push
|
||||
docker pull alpine:latest
|
||||
docker tag alpine:latest registry.leeworks.dev/leeworks-agents/test:latest
|
||||
docker push registry.leeworks.dev/leeworks-agents/test:latest
|
||||
|
||||
# Test pull from cluster
|
||||
kubectl run test-pull --image=registry.leeworks.dev/leeworks-agents/test:latest \
|
||||
--image-pull-policy=Always --rm -it --restart=Never -- echo "Registry works"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 Reference
|
||||
|
||||
All API repos should update their `ROADMAP.md §Phase 4` to reference:
|
||||
```
|
||||
registry.leeworks.dev/leeworks-agents/<repo>:<tag>
|
||||
```
|
||||
as the image target for CI pushes and Flux HelmRelease image references.
|
||||
@@ -1,333 +0,0 @@
|
||||
# Kubernetes Secrets Checklist
|
||||
|
||||
All infrastructure blockers reduce to creating six Kubernetes secrets and one Gitea Actions secret.
|
||||
Follow this list top-to-bottom; each step unblocks the next.
|
||||
|
||||
**Human operator only** — the agent cannot log into Gitea's admin panel or run `kubectl` in the cluster.
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] 1. `gitea-leeworks-agents-token` (flux-system) — unblocks Flux GitRepository auth
|
||||
- [ ] 2. `gitea-runner-token` (gitea-runner) — unblocks Gitea Actions runner registration
|
||||
- [ ] 3. `grafana-admin` (monitoring) — unblocks Grafana login
|
||||
- [ ] 4. `gatus-slack-webhook` (monitoring) — unblocks Gatus alert notifications
|
||||
- [ ] 5. `GITEA_TOKEN` in each API repo's Actions Secrets — unblocks CI image push
|
||||
- [ ] 6. Gitea packages enabled + DNS record for `registry.leeworks.dev` — unblocks image push to registry
|
||||
- [ ] 7. Add api-company Flux source + kustomization to 0xWheatyz/Talos — unblocks all GitOps reconciliation
|
||||
- [ ] 8. `gitea-registry` (zip-enrichment, holidays, air-quality, docs-site) — imagePullSecret for pods pulling from `registry.leeworks.dev`
|
||||
- [ ] 9. `gitea-image-automation-token` (flux-system) — write-scoped token for Flux ImageUpdateAutomation to push image-tag commits
|
||||
- [ ] 10. `rapidapi-proxy-secret` (zip-enrichment, holidays, air-quality) — RapidAPI Proxy Secret for server-side request validation
|
||||
- [ ] 11. `GITEA_TOKEN` Actions secret in `leeworks-agents/vin-decoder` repo — enables CI image push for VIN Decoder (leeworks-agents/api-company#126)
|
||||
- [ ] 12. `gitea-registry` imagePullSecret in `vin-decoder` namespace — enables pod image pulls from `registry.leeworks.dev` (leeworks-agents/api-company#127)
|
||||
- [ ] 13. `rapidapi-proxy-secret` in `vin-decoder` namespace — enables RapidAPI proxy-secret header validation (leeworks-agents/api-company#128)
|
||||
|
||||
---
|
||||
|
||||
## Secret Details
|
||||
|
||||
### 1. `gitea-leeworks-agents-token`
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `gitea-leeworks-agents-token` |
|
||||
| Namespace | `flux-system` |
|
||||
| Purpose | Flux `GitRepository` authenticates to Gitea over HTTPS to pull `leeworks-agents/api-company` |
|
||||
| Source | Gitea web UI → User Settings → Applications → Generate Token (scopes: `read:repository`) |
|
||||
| Unblocks | Issue #2 (Flux GitRepository + Kustomization for api-company) |
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gitea-leeworks-agents-token \
|
||||
-n flux-system \
|
||||
--from-literal=username=leeworks-agents \
|
||||
--from-literal=password=<GITEA_TOKEN>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `gitea-runner-token`
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `gitea-runner-token` |
|
||||
| Namespace | `gitea-runner` |
|
||||
| Purpose | The `gitea-act-runner` HelmRelease reads this token to register the runner with Gitea |
|
||||
| Source | Gitea Admin Panel → Site Administration → Actions → Runners → **Create new Runner** — copy registration token |
|
||||
| Unblocks | Issue #3 (gitea-act-runner Flux deployment) |
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gitea-runner-token \
|
||||
-n gitea-runner \
|
||||
--from-literal=token=<RUNNER_TOKEN>
|
||||
```
|
||||
|
||||
After creating the secret, Flux reconciles the `gitea-act-runner` HelmRelease and the runner appears as **Online** in Gitea Admin → Actions → Runners.
|
||||
|
||||
---
|
||||
|
||||
### 3. `grafana-admin`
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `grafana-admin` |
|
||||
| Namespace | `monitoring` |
|
||||
| Purpose | Sets the Grafana `admin` user password on first boot |
|
||||
| Source | Choose a strong password and store it in a password manager |
|
||||
| Unblocks | Issue #7 (Prometheus + Grafana HelmRelease) |
|
||||
|
||||
```bash
|
||||
kubectl create secret generic grafana-admin \
|
||||
-n monitoring \
|
||||
--from-literal=admin-password=<PASSWORD>
|
||||
```
|
||||
|
||||
Grafana will be accessible at `https://grafana.leeworks.dev` (login: `admin` / `<PASSWORD>`).
|
||||
|
||||
---
|
||||
|
||||
### 4. `gatus-slack-webhook`
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `gatus-slack-webhook` |
|
||||
| Namespace | `monitoring` |
|
||||
| Purpose | Gatus posts downtime alerts to a Slack channel via incoming webhook |
|
||||
| Source | Slack → Your workspace → Apps → Incoming Webhooks → Add to Slack → copy webhook URL |
|
||||
| Unblocks | Issue #8 (Gatus status page at `status.leeworks.dev`) |
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gatus-slack-webhook \
|
||||
-n monitoring \
|
||||
--from-literal=url=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. `GITEA_TOKEN` — Gitea Actions Secret (per repo)
|
||||
|
||||
| Field | Value |
|
||||
|----------|-------|
|
||||
| Name | `GITEA_TOKEN` |
|
||||
| Scope | Gitea Actions Secret — set in each repo's Settings, **not** a Kubernetes secret |
|
||||
| Purpose | CI workflows use this token to push container images to `registry.leeworks.dev` |
|
||||
| Source | Same token as step 1, or a dedicated CI token with `write:packages` scope |
|
||||
| Unblocks | CI pipelines for all three API repos |
|
||||
|
||||
Set in Gitea web UI for **each** of these repos:
|
||||
- `leeworks-agents/api-company`
|
||||
- `leeworks-agents/zip-enrichment`
|
||||
- `leeworks-agents/holidays`
|
||||
- `leeworks-agents/air-quality`
|
||||
|
||||
Path: **Repo → Settings → Actions → Secrets → Add Secret**
|
||||
- Name: `GITEA_TOKEN`
|
||||
- Value: `<GITEA_TOKEN>`
|
||||
|
||||
---
|
||||
|
||||
### 6. Enable Gitea Packages + DNS for `registry.leeworks.dev`
|
||||
|
||||
This is a Gitea instance configuration step, not a Kubernetes secret.
|
||||
|
||||
| Step | Action |
|
||||
|------|--------|
|
||||
| 6a | Enable packages in Gitea `app.ini`: set `[packages] ENABLED = true` then restart Gitea |
|
||||
| 6b | Add DNS A record: `registry.leeworks.dev` → cluster ingress IP |
|
||||
|
||||
Find cluster ingress IP:
|
||||
```bash
|
||||
kubectl get svc -n ingress-nginx
|
||||
```
|
||||
|
||||
See `docs/registry.md` for context on why the Gitea built-in registry was chosen.
|
||||
|
||||
Unblocks: Issue #4 (container registry), and transitively all CI image-push workflows.
|
||||
|
||||
---
|
||||
|
||||
### 7. Add api-company Flux Source + Kustomization to 0xWheatyz/Talos
|
||||
|
||||
Reference manifests are already committed at `flux/api-company-source/` in this repo.
|
||||
The operator must copy them into the Talos cluster repo so FluxCD picks them up:
|
||||
|
||||
```
|
||||
0xWheatyz/Talos:testing1/first-cluster/cluster/flux/api-company-source/
|
||||
```
|
||||
|
||||
Unblocks: Issue #2 (Flux reconciliation of all `flux/` manifests in this repo).
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
### 10. `rapidapi-proxy-secret` — RapidAPI Proxy Secret (per API namespace)
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `rapidapi-proxy-secret` |
|
||||
| Namespaces | `zip-enrichment`, `holidays`, `air-quality` |
|
||||
| Purpose | Every API service validates the `X-RapidAPI-Proxy-Secret` header on every route. Requests without a valid secret return HTTP 403. |
|
||||
| Source | RapidAPI dashboard → API Settings → Security → **Proxy Secret** (generated after each API listing is created) |
|
||||
| Unblocks | Phase 3 server middleware; API services will start but reject all traffic without this secret |
|
||||
|
||||
```bash
|
||||
for NS in zip-enrichment holidays air-quality; do
|
||||
kubectl create secret generic rapidapi-proxy-secret \
|
||||
--namespace=$NS \
|
||||
--from-literal=X-RapidAPI-Proxy-Secret=<value-from-rapidapi-dashboard>
|
||||
done
|
||||
```
|
||||
|
||||
**Source:** RapidAPI dashboard → select your API → Settings → Security → Proxy Secret
|
||||
|
||||
> **Note:** Placeholder `ExternalSecret` manifests are committed at
|
||||
> `flux/zip-enrichment/externalsecret.yaml`, `flux/holidays/externalsecret.yaml`,
|
||||
> and `flux/air-quality/externalsecret.yaml`. These will auto-sync this secret
|
||||
> from the configured backend once the External Secrets Operator (ESO) is
|
||||
> deployed (see issue #61). Until then, create manually using the commands above.
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
for NS in zip-enrichment holidays air-quality; do
|
||||
echo -n "$NS: "
|
||||
kubectl get secret rapidapi-proxy-secret -n $NS -o jsonpath='{.data.X-RapidAPI-Proxy-Secret}' | base64 -d | wc -c
|
||||
echo " chars"
|
||||
done
|
||||
```
|
||||
|
||||
|
||||
### 11. `GITEA_TOKEN` Actions secret in `leeworks-agents/vin-decoder`
|
||||
|
||||
| Field | Value |
|
||||
|----------|-------|
|
||||
| Name | `GITEA_TOKEN` |
|
||||
| Scope | Gitea Actions Secret — set in repo Settings |
|
||||
| Purpose | CI workflow pushes container image to `registry.leeworks.dev/vin-decoder/api:<sha>` |
|
||||
| Source | Gitea token with `write:packages` scope (reuse from item #5 if it has `write:packages`) |
|
||||
| Tracked | leeworks-agents/api-company#126 |
|
||||
| Unblocks | CI image push for VIN Decoder |
|
||||
|
||||
Path: **Gitea → leeworks-agents/vin-decoder → Settings → Secrets → `GITEA_TOKEN`**
|
||||
|
||||
---
|
||||
|
||||
### 12. `gitea-registry` imagePullSecret in `vin-decoder` namespace
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `gitea-registry` |
|
||||
| Namespace | `vin-decoder` |
|
||||
| Type | `kubernetes.io/dockerconfigjson` |
|
||||
| Purpose | Allows VIN Decoder pods to pull images from `registry.leeworks.dev` without ImagePullBackOff |
|
||||
| Tracked | leeworks-agents/api-company#127 |
|
||||
|
||||
```bash
|
||||
kubectl create namespace vin-decoder --dry-run=client -o yaml | kubectl apply -f -
|
||||
kubectl create secret docker-registry gitea-registry \
|
||||
--namespace=vin-decoder \
|
||||
--docker-server=registry.leeworks.dev \
|
||||
--docker-username=leeworks-agents \
|
||||
--docker-password=<GITEA_TOKEN_WITH_READ_PACKAGES> \
|
||||
--docker-email=agent@leeworks.dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13. `rapidapi-proxy-secret` in `vin-decoder` namespace
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `rapidapi-proxy-secret` |
|
||||
| Namespace | `vin-decoder` |
|
||||
| Purpose | VIN Decoder validates `X-RapidAPI-Proxy-Secret` header; returns HTTP 403 if missing/wrong |
|
||||
| Source | RapidAPI dashboard → VIN Decoder listing → Settings → Security → Proxy Secret |
|
||||
| Tracked | leeworks-agents/api-company#128 |
|
||||
|
||||
```bash
|
||||
# Placeholder (unblocks deploy testing):
|
||||
kubectl create secret generic rapidapi-proxy-secret \
|
||||
--namespace=vin-decoder \
|
||||
--from-literal=X-RapidAPI-Proxy-Secret=PLACEHOLDER_REPLACE_AFTER_RAPIDAPI_LISTING
|
||||
|
||||
# Update with real value after RapidAPI listing (#131) is live:
|
||||
kubectl create secret generic rapidapi-proxy-secret \
|
||||
-n vin-decoder \
|
||||
--from-literal=X-RapidAPI-Proxy-Secret=<VIN_RAPIDAPI_PROXY_SECRET> \
|
||||
--save-config --dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Order
|
||||
|
||||
```
|
||||
7 (Flux wiring) → all flux/ resources reconcile
|
||||
1 (gitea-leeworks-token) → Flux can pull this repo over HTTPS
|
||||
2 (gitea-runner-token) → runner online → CI runs
|
||||
3 (grafana-admin) → Grafana login works
|
||||
4 (gatus-slack-webhook) → Gatus alerting works
|
||||
5 + 6 (GITEA_TOKEN + registry packages) → CI pushes images → API services deploy
|
||||
8 (gitea-registry) → pods can pull images from registry.leeworks.dev → services start
|
||||
9 (gitea-image-automation-token) → Flux ImageUpdateAutomation pushes tag-update commits
|
||||
10 (rapidapi-proxy-secret × 3) → API server middleware validates RapidAPI requests → revenue enabled
|
||||
```
|
||||
|
||||
Once all nine items are complete, the full stack (runner, registry, Prometheus, Grafana, Gatus, docs-site, three API services) reconciles automatically via FluxCD with no further manual steps.
|
||||
|
||||
---
|
||||
|
||||
### 8. `gitea-registry` — imagePullSecret for API service namespaces
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `gitea-registry` |
|
||||
| Namespaces | `zip-enrichment`, `holidays`, `air-quality`, `docs-site` |
|
||||
| Type | `kubernetes.io/dockerconfigjson` |
|
||||
| Purpose | Allows pods to pull images from `registry.leeworks.dev` without ImagePullBackOff |
|
||||
| Source | Gitea token with `read:packages` scope (can reuse the same token as step 1 if it has that scope) |
|
||||
| Unblocks | Issues #58 (Phase 0 hard deploy blocker) and transitively Phase 3 service deploys |
|
||||
|
||||
```bash
|
||||
for NS in zip-enrichment holidays air-quality docs-site; do
|
||||
kubectl create secret docker-registry gitea-registry \
|
||||
--namespace=$NS \
|
||||
--docker-server=registry.leeworks.dev \
|
||||
--docker-username=leeworks-agents \
|
||||
--docker-password=<GITEA_TOKEN_WITH_READ_PACKAGES> \
|
||||
--docker-email=agent@leeworks.dev
|
||||
done
|
||||
```
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
kubectl get secret gitea-registry -n zip-enrichment -o jsonpath='{.type}'
|
||||
# expected: kubernetes.io/dockerconfigjson
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. `gitea-image-automation-token` — Flux ImageUpdateAutomation write token
|
||||
|
||||
| Field | Value |
|
||||
|-----------|-------|
|
||||
| Name | `gitea-image-automation-token` |
|
||||
| Namespace | `flux-system` |
|
||||
| Purpose | Allows Flux `ImageUpdateAutomation` to push image-tag update commits back to `leeworks-agents/api-company` |
|
||||
| Source | Gitea token with **`write:repository`** scope (the existing `gitea-leeworks-agents-token` only has `read:repository` — create a separate token or verify scope) |
|
||||
| Unblocks | Issue #57 (Flux ImageUpdateAutomation for api-company) |
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gitea-image-automation-token \
|
||||
-n flux-system \
|
||||
--from-literal=username=leeworks-agents \
|
||||
--from-literal=password=<TOKEN_WITH_WRITE_REPO_SCOPE>
|
||||
```
|
||||
|
||||
Verify after creation:
|
||||
```bash
|
||||
flux get imageupdateautomations -n flux-system
|
||||
# Expected: api-company shows READY=True
|
||||
```
|
||||
|
||||
> **Note:** If you create a new token with `write:repository` scope, keep the existing `gitea-leeworks-agents-token` for read-only Flux GitRepository pulls and use this new secret exclusively for `ImageUpdateAutomation`.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Tooling Versions
|
||||
|
||||
> Phase 0 checklist item — documents the local agent container tooling and confirms git remote config.
|
||||
> Live cluster verification (kubectl/flux against the running cluster) requires operator confirmation.
|
||||
|
||||
## Agent Container Tooling
|
||||
|
||||
| Tool | Version | Source |
|
||||
|------|---------|--------|
|
||||
| Node.js | v22.22.2 | `node --version` |
|
||||
| Python 3 | 3.11.2 | `python3 --version` |
|
||||
| Helm | v3.21.0 | `helm version --short` |
|
||||
| Flux CLI | v2.4.0 | `flux version --client` |
|
||||
| kustomize | v5.6.0 | `kustomize version` |
|
||||
| kubectl | available | `kubectl` (cluster context not set in agent container — live commands require kubeconfig from operator) |
|
||||
|
||||
## Git Remote Config
|
||||
|
||||
```
|
||||
origin ssh://git@gitea.leeworks.dev/leeworks-agents/api-company (fetch)
|
||||
origin ssh://git@gitea.leeworks.dev/leeworks-agents/api-company (push)
|
||||
upstream ssh://git@gitea.leeworks.dev/0xWheatyz/api-company (fetch)
|
||||
upstream ssh://git@gitea.leeworks.dev/0xWheatyz/api-company (push)
|
||||
```
|
||||
|
||||
Remote `origin` confirmed pointing to `gitea.leeworks.dev/leeworks-agents/api-company`.
|
||||
|
||||
> Note: `0xWheatyz/api-company` upstream repo does not yet exist on Gitea — tracked by issue #47.
|
||||
|
||||
## Stack Summary
|
||||
|
||||
### Runtime choices
|
||||
|
||||
| API Service | Runtime | Rationale |
|
||||
|-------------|---------|-----------|
|
||||
| zip-enrichment | Node.js (Fastify) | Lightweight, fast JSON serialisation; large ZIP dataset fits SQLite well |
|
||||
| holidays | Node.js (Fastify) | Simple lookup API; Fastify handles high req/s with low memory |
|
||||
| air-quality | Python (FastAPI) | AQI ingestion benefits from Python data-science ecosystem |
|
||||
| docs-site | Astro (static) | Zero-runtime static site; built by CI, served from container |
|
||||
|
||||
### Helm chart registry
|
||||
|
||||
All HelmReleases use charts sourced from public Helm chart repositories declared in `flux/` as `HelmRepository` resources:
|
||||
|
||||
- `flux/gitea-runner/helmrepository.yaml` — Gitea Act Runner chart
|
||||
- `flux/monitoring/` — `kube-prometheus-stack` and Gatus charts
|
||||
- `flux/zip-enrichment/`, `flux/holidays/`, `flux/air-quality/` — per-API service charts
|
||||
|
||||
The in-cluster container registry is `registry.leeworks.dev` (Gitea built-in packages/container registry, documented in `docs/registry.md`).
|
||||
|
||||
### Flux version and bootstrap
|
||||
|
||||
- **Flux CLI**: v2.4.0 (available in agent container for manifest authoring)
|
||||
- **Flux controllers**: bootstrapped into `0xWheatyz/Talos` cluster (FluxCD v2.x)
|
||||
- `flux-system` namespace managed by Talos GitOps repo
|
||||
- Flux watches `0xWheatyz/Talos` → `testing1/first-cluster/cluster/`
|
||||
- `api-company-source` GitRepository + Kustomization adds this repo to Flux (issues #2, #90)
|
||||
- **Bootstrap reference**: `0xWheatyz/Talos` repo — see Talos cluster documentation
|
||||
|
||||
## Live Verification (Operator)
|
||||
|
||||
The following commands require a valid `kubeconfig` (not available in agent container):
|
||||
|
||||
```bash
|
||||
kubectl config current-context # confirm cluster context
|
||||
kubectl version --client # kubectl client version
|
||||
flux check # Flux controller health
|
||||
flux get sources git api-company # GitRepository READY status
|
||||
flux get kustomizations api-company # Kustomization READY status
|
||||
helm version # Helm client version
|
||||
kustomize version # kustomize version
|
||||
```
|
||||
|
||||
_Reference: MASTER_BUILD_PROMPT.md §Phase 0 step 2 — Closes leeworks-agents/api-company#94_
|
||||
@@ -0,0 +1 @@
|
||||
# placeholder — populated by Phase-4/5 issues
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: rapidapi-proxy-secret
|
||||
namespace: air-quality
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: kubernetes-provider
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: rapidapi-proxy-secret
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: X-RapidAPI-Proxy-Secret
|
||||
remoteRef:
|
||||
key: rapidapi-air-quality-source
|
||||
property: X-RapidAPI-Proxy-Secret
|
||||
@@ -1,101 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: air-quality
|
||||
spec:
|
||||
interval: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: raw
|
||||
version: ">=0.2.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: bedag
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
resources:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: air-quality
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: air-quality
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: air-quality
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
containers:
|
||||
- name: air-quality
|
||||
image: registry.leeworks.dev/air-quality/server:latest # {"$imagepolicy": "flux-system:air-quality"}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: RAPIDAPI_PROXY_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rapidapi-proxy-secret
|
||||
key: X-RapidAPI-Proxy-Secret
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: air-quality
|
||||
spec:
|
||||
selector:
|
||||
app: air-quality
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
- apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: air-quality
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- aqi.leeworks.dev
|
||||
secretName: air-quality-tls
|
||||
rules:
|
||||
- host: aqi.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: air-quality
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
- servicemonitor.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: air-quality
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: air-quality
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: air-quality
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -1,17 +0,0 @@
|
||||
# This manifest is FOR REFERENCE — the live version must be committed to
|
||||
# 0xWheatyz/Talos at testing1/first-cluster/cluster/flux/api-company/
|
||||
#
|
||||
# See leeworks-agents/api-company#2
|
||||
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: api-company
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
url: https://gitea.leeworks.dev/leeworks-agents/api-company
|
||||
ref:
|
||||
branch: main
|
||||
secretRef:
|
||||
name: gitea-leeworks-agents-token # must pre-exist in flux-system ns
|
||||
@@ -1,25 +0,0 @@
|
||||
# This manifest is FOR REFERENCE — the live version must be committed to
|
||||
# 0xWheatyz/Talos at testing1/first-cluster/cluster/flux/api-company-source/
|
||||
#
|
||||
# See leeworks-agents/api-company#2
|
||||
# See leeworks-agents/api-company#97
|
||||
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: api-company
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: api-company
|
||||
path: ./flux
|
||||
prune: true
|
||||
wait: true
|
||||
timeout: 5m
|
||||
postBuild:
|
||||
substituteFrom:
|
||||
- kind: Secret
|
||||
name: grafana-admin
|
||||
optional: false
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: bedag
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 60m
|
||||
url: https://bedag.github.io/helm-charts/
|
||||
@@ -1,89 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: docs-site
|
||||
namespace: docs-site
|
||||
spec:
|
||||
interval: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: raw
|
||||
version: ">=0.2.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: bedag
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
resources:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docs-site
|
||||
namespace: docs-site
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docs-site
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docs-site
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
containers:
|
||||
- name: docs-site
|
||||
image: registry.leeworks.dev/leeworks-agents/docs-site:latest # {"$imagepolicy": "flux-system:docs-site"}
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 30
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docs-site
|
||||
namespace: docs-site
|
||||
spec:
|
||||
selector:
|
||||
app: docs-site
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
- apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: docs-site
|
||||
namespace: docs-site
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- docs.leeworks.dev
|
||||
secretName: docs-site-tls
|
||||
rules:
|
||||
- host: docs.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: docs-site
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: docs-site
|
||||
@@ -1,50 +0,0 @@
|
||||
# ClusterSecretStore using the Kubernetes provider.
|
||||
# Reads secrets from the cluster itself — no external vault required.
|
||||
# The service account below must have get/list access to secrets in
|
||||
# the namespaces where ExternalSecrets are created.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: eso-kubernetes-provider
|
||||
namespace: external-secrets
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: eso-kubernetes-provider-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: eso-kubernetes-provider-reader
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: eso-kubernetes-provider-reader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: eso-kubernetes-provider
|
||||
namespace: external-secrets
|
||||
---
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ClusterSecretStore
|
||||
metadata:
|
||||
name: kubernetes-provider
|
||||
spec:
|
||||
provider:
|
||||
kubernetes:
|
||||
remoteNamespace: external-secrets
|
||||
server:
|
||||
caProvider:
|
||||
type: ConfigMap
|
||||
name: kube-root-ca.crt
|
||||
namespace: external-secrets
|
||||
key: ca.crt
|
||||
auth:
|
||||
serviceAccount:
|
||||
name: eso-kubernetes-provider
|
||||
namespace: external-secrets
|
||||
@@ -1,30 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: external-secrets
|
||||
namespace: external-secrets
|
||||
spec:
|
||||
interval: 15m
|
||||
chart:
|
||||
spec:
|
||||
chart: external-secrets
|
||||
version: ">=0.9.0 <1.0.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: external-secrets
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
install:
|
||||
crds: CreateReplace
|
||||
remediation:
|
||||
retries: 3
|
||||
upgrade:
|
||||
crds: CreateReplace
|
||||
remediation:
|
||||
retries: 3
|
||||
values:
|
||||
installCRDs: true
|
||||
webhook:
|
||||
port: 9443
|
||||
certController:
|
||||
requeueInterval: 5m
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: external-secrets
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 60m
|
||||
url: https://charts.external-secrets.io
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
- clustersecretstore.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: external-secrets
|
||||
@@ -1,42 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: gitea-act-runner
|
||||
namespace: gitea-runner
|
||||
spec:
|
||||
interval: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: gitea-act-runner
|
||||
version: ">=0.1.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: gitea-charts
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
replicaCount: 1
|
||||
config:
|
||||
registration:
|
||||
# Gitea instance URL
|
||||
instanceUrl: "https://gitea.leeworks.dev"
|
||||
# Token from Gitea admin → Actions → Runners → New Runner
|
||||
# Store in a Kubernetes Secret named gitea-runner-token
|
||||
tokenFromSecret:
|
||||
secretName: gitea-runner-token
|
||||
secretKey: token
|
||||
runner:
|
||||
# Register at org scope so all leeworks-agents repos can use it
|
||||
labels:
|
||||
- "ubuntu-latest:docker://node:20-bookworm"
|
||||
- "ubuntu-22.04:docker://node:20-bookworm"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
# Runner needs Docker socket or dind
|
||||
dind:
|
||||
enabled: true
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: gitea-charts
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 60m
|
||||
url: https://dl.gitea.com/charts/
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gitea-runner
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: rapidapi-proxy-secret
|
||||
namespace: holidays
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: kubernetes-provider
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: rapidapi-proxy-secret
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: X-RapidAPI-Proxy-Secret
|
||||
remoteRef:
|
||||
key: rapidapi-holidays-source
|
||||
property: X-RapidAPI-Proxy-Secret
|
||||
@@ -1,101 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: holidays
|
||||
spec:
|
||||
interval: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: raw
|
||||
version: ">=0.2.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: bedag
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
resources:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: holidays
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: holidays
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: holidays
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
containers:
|
||||
- name: holidays
|
||||
image: registry.leeworks.dev/holidays/server:latest # {"$imagepolicy": "flux-system:holidays"}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: RAPIDAPI_PROXY_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rapidapi-proxy-secret
|
||||
key: X-RapidAPI-Proxy-Secret
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: holidays
|
||||
spec:
|
||||
selector:
|
||||
app: holidays
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
- apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: holidays
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- holidays.leeworks.dev
|
||||
secretName: holidays-tls
|
||||
rules:
|
||||
- host: holidays.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: holidays
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
- servicemonitor.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: holidays
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: holidays
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: holidays
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -1,63 +0,0 @@
|
||||
# ImagePolicy: select the latest semver tag from each ImageRepository.
|
||||
# Tags pushed by CI should follow semver (e.g. v1.2.3) or use "latest" —
|
||||
# the semver policy picks up any vX.Y.Z tag. The "latest" alias keeps
|
||||
# things working before formal releases are tagged.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: flux-system
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: zip-enrichment
|
||||
policy:
|
||||
semver:
|
||||
range: ">=0.1.0"
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: flux-system
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: holidays
|
||||
policy:
|
||||
semver:
|
||||
range: ">=0.1.0"
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: flux-system
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: air-quality
|
||||
policy:
|
||||
semver:
|
||||
range: ">=0.1.0"
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
namespace: flux-system
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: vin-decoder
|
||||
policy:
|
||||
semver:
|
||||
range: ">=0.1.0"
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: docs-site
|
||||
namespace: flux-system
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: docs-site
|
||||
policy:
|
||||
alphabetical:
|
||||
order: asc
|
||||
@@ -1,56 +0,0 @@
|
||||
# Flux image-reflector-controller watches these registries for new image tags.
|
||||
# Requires: flux-system/gitea-image-automation-token secret (see docs/operator-runbook.md)
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: registry.leeworks.dev/zip-enrichment/server
|
||||
interval: 5m
|
||||
secretRef:
|
||||
name: gitea-leeworks-agents-token
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: registry.leeworks.dev/holidays/server
|
||||
interval: 5m
|
||||
secretRef:
|
||||
name: gitea-leeworks-agents-token
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: registry.leeworks.dev/air-quality/server
|
||||
interval: 5m
|
||||
secretRef:
|
||||
name: gitea-leeworks-agents-token
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: registry.leeworks.dev/vin-decoder/api
|
||||
interval: 5m
|
||||
secretRef:
|
||||
name: gitea-leeworks-agents-token
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: docs-site
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: registry.leeworks.dev/leeworks-agents/docs-site
|
||||
interval: 5m
|
||||
secretRef:
|
||||
name: gitea-leeworks-agents-token
|
||||
@@ -1,39 +0,0 @@
|
||||
# ImageUpdateAutomation: when an ImagePolicy selects a new tag, this object
|
||||
# instructs Flux to open a commit on the api-company repo updating the
|
||||
# image reference in the relevant HelmRelease values.
|
||||
#
|
||||
# The GitRepository used here is the api-company source (flux-system/api-company).
|
||||
# Flux needs write access; create the token secret first:
|
||||
# kubectl create secret generic gitea-image-automation-token \
|
||||
# -n flux-system \
|
||||
# --from-literal=username=leeworks-agents \
|
||||
# --from-literal=password=<TOKEN_WITH_WRITE_REPO>
|
||||
# Then patch the api-company GitRepository to reference it (or reuse
|
||||
# gitea-leeworks-agents-token if that token also has write:repository scope).
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: api-company
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: api-company
|
||||
git:
|
||||
checkout:
|
||||
ref:
|
||||
branch: main
|
||||
commit:
|
||||
author:
|
||||
email: agent@leeworks.dev
|
||||
name: Flux Image Automation
|
||||
messageTemplate: |
|
||||
chore(image): update {{range .Updated.Images}}{{.Repository}}:{{.NewTag}} {{end}}
|
||||
push:
|
||||
branch: main
|
||||
secretRef:
|
||||
name: gitea-image-automation-token # must pre-exist in flux-system ns — see docs/secrets-checklist.md item 9
|
||||
update:
|
||||
path: ./flux
|
||||
strategy: Setters
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- imagerepositories.yaml
|
||||
- imagepolicies.yaml
|
||||
- imageupdateautomation.yaml
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- bedag-helmrepository.yaml
|
||||
- external-secrets
|
||||
- gitea-runner
|
||||
- monitoring
|
||||
- docs-site
|
||||
- zip-enrichment
|
||||
- holidays
|
||||
- air-quality
|
||||
- vin-decoder
|
||||
- image-automation
|
||||
@@ -1,103 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: gatus
|
||||
namespace: monitoring
|
||||
spec:
|
||||
interval: 15m
|
||||
chart:
|
||||
spec:
|
||||
chart: gatus
|
||||
version: ">=1.0.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: minicloudlabs
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: status.leeworks.dev
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gatus-tls
|
||||
hosts:
|
||||
- status.leeworks.dev
|
||||
config:
|
||||
storage:
|
||||
type: sqlite
|
||||
path: /data/gatus.db
|
||||
endpoints:
|
||||
- name: ZIP Enrichment API
|
||||
url: https://zip.leeworks.dev/health
|
||||
interval: 1m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[RESPONSE_TIME] < 1000"
|
||||
alerts:
|
||||
- type: slack
|
||||
description: "ZIP Enrichment API is down"
|
||||
send-on-resolved: true
|
||||
|
||||
- name: Holidays API
|
||||
url: https://holidays.leeworks.dev/health
|
||||
interval: 1m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[RESPONSE_TIME] < 1000"
|
||||
alerts:
|
||||
- type: slack
|
||||
description: "Holidays API is down"
|
||||
send-on-resolved: true
|
||||
|
||||
- name: Air Quality API
|
||||
url: https://aqi.leeworks.dev/health
|
||||
interval: 1m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[RESPONSE_TIME] < 1000"
|
||||
alerts:
|
||||
- type: slack
|
||||
description: "Air Quality API is down"
|
||||
send-on-resolved: true
|
||||
|
||||
|
||||
- name: VIN Decoder API
|
||||
url: https://vin.leeworks.dev/v1/health
|
||||
interval: 1m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[RESPONSE_TIME] < 1000"
|
||||
alerts:
|
||||
- type: slack
|
||||
- name: Docs Site
|
||||
url: https://docs.leeworks.dev
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: Container Registry
|
||||
url: https://registry.leeworks.dev/v2/
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
ui:
|
||||
title: "leeworks.dev API Status"
|
||||
description: "Real-time status for all leeworks.dev APIs"
|
||||
logo: ""
|
||||
# Retention: 90 days
|
||||
retention:
|
||||
days: 90
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 1Gi
|
||||
mountPath: /data
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: minicloudlabs
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 60m
|
||||
url: https://minicloudlabs.github.io/helm-charts
|
||||
@@ -1,190 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: grafana-dashboard-apis
|
||||
namespace: monitoring
|
||||
labels:
|
||||
grafana_dashboard: "1"
|
||||
data:
|
||||
api-dashboard.json: |
|
||||
{
|
||||
"annotations": { "list": [] },
|
||||
"description": "Request rate, latency, error rate, and data freshness for zip-enrichment, holidays, and air-quality APIs",
|
||||
"editable": true,
|
||||
"graphTooltip": 1,
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"title": "Request Rate",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": { "axisLabel": "requests/sec", "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" },
|
||||
"unit": "reqps"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 },
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum by (api, route) (rate(api_requests_total{api=~\"zip-enrichment|holidays|air-quality\"}[5m]))",
|
||||
"legendFormat": "{{api}} {{route}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Request Rate by API / Route",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": { "axisLabel": "error fraction", "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" },
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.05 },
|
||||
{ "color": "red", "value": 0.20 }
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 },
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum by (api) (rate(api_requests_total{api=~\"zip-enrichment|holidays|air-quality\",status=~\"5..\"}[5m])) / sum by (api) (rate(api_requests_total{api=~\"zip-enrichment|holidays|air-quality\"}[5m]))",
|
||||
"legendFormat": "{{api}} 5xx error rate",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "5xx Error Rate by API",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 },
|
||||
"id": 4,
|
||||
"title": "Latency P50 / P95 / P99",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": { "axisLabel": "seconds", "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" },
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 1.0 },
|
||||
{ "color": "red", "value": 2.0 }
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 10 },
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "histogram_quantile(0.50, sum by (api, le) (rate(api_response_duration_seconds_bucket{api=~\"zip-enrichment|holidays|air-quality\"}[5m])))",
|
||||
"legendFormat": "P50 {{api}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, sum by (api, le) (rate(api_response_duration_seconds_bucket{api=~\"zip-enrichment|holidays|air-quality\"}[5m])))",
|
||||
"legendFormat": "P95 {{api}}",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.99, sum by (api, le) (rate(api_response_duration_seconds_bucket{api=~\"zip-enrichment|holidays|air-quality\"}[5m])))",
|
||||
"legendFormat": "P99 {{api}}",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Response Latency P50 / P95 / P99 by API",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 18 },
|
||||
"id": 6,
|
||||
"title": "Data Freshness",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"max": 2592000,
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 1296000 },
|
||||
{ "color": "red", "value": 2592000 }
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 19 },
|
||||
"id": 7,
|
||||
"options": {
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "api_data_freshness_seconds{api=~\"zip-enrichment|holidays|air-quality\"}",
|
||||
"legendFormat": "{{api}} ({{dataset}})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Data Freshness — alert threshold at 30 days (2592000 s)",
|
||||
"type": "gauge"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 38,
|
||||
"tags": ["api-company", "leeworks"],
|
||||
"templating": { "list": [] },
|
||||
"time": { "from": "now-3h", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "leeworks.dev API Metrics",
|
||||
"uid": "leeworks-api-metrics",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: kube-prometheus-stack
|
||||
namespace: monitoring
|
||||
spec:
|
||||
interval: 15m
|
||||
chart:
|
||||
spec:
|
||||
chart: kube-prometheus-stack
|
||||
version: ">=58.0.0 <60.0.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: prometheus-community
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
install:
|
||||
crds: CreateReplace
|
||||
remediation:
|
||||
retries: 3
|
||||
upgrade:
|
||||
crds: CreateReplace
|
||||
remediation:
|
||||
retries: 3
|
||||
values:
|
||||
grafana:
|
||||
enabled: true
|
||||
adminPassword: "${GRAFANA_ADMIN_PASSWORD}" # inject via Secret/substitution
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- grafana.leeworks.dev
|
||||
tls:
|
||||
- secretName: grafana-tls
|
||||
hosts:
|
||||
- grafana.leeworks.dev
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
sidecar:
|
||||
dashboards:
|
||||
enabled: true
|
||||
prometheus:
|
||||
prometheusSpec:
|
||||
retention: 30d
|
||||
storageSpec:
|
||||
volumeClaimTemplate:
|
||||
spec:
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
# Scrape pods with prometheus.io/scrape=true annotations
|
||||
podMonitorNamespaceSelector: {}
|
||||
podMonitorSelector: {}
|
||||
serviceMonitorNamespaceSelector: {}
|
||||
serviceMonitorSelector: {}
|
||||
# Additional scrape configs for annotation-based discovery
|
||||
additionalScrapeConfigs:
|
||||
- job_name: 'kubernetes-pods'
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
|
||||
action: keep
|
||||
regex: "true"
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
|
||||
action: replace
|
||||
target_label: __metrics_path__
|
||||
regex: (.+)
|
||||
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
|
||||
action: replace
|
||||
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||
replacement: $1:$2
|
||||
target_label: __address__
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_pod_label_(.+)
|
||||
- source_labels: [__meta_kubernetes_namespace]
|
||||
action: replace
|
||||
target_label: kubernetes_namespace
|
||||
- source_labels: [__meta_kubernetes_pod_name]
|
||||
action: replace
|
||||
target_label: kubernetes_pod_name
|
||||
alertmanager:
|
||||
enabled: false # Enable when alert routing is configured
|
||||
kubeStateMetrics:
|
||||
enabled: true
|
||||
nodeExporter:
|
||||
enabled: true
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: prometheus-community
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 60m
|
||||
url: https://prometheus-community.github.io/helm-charts
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
- gatus-helmrepository.yaml
|
||||
- gatus-helmrelease.yaml
|
||||
- grafana-dashboard-apis.yaml
|
||||
- prometheusrule-apis.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: monitoring
|
||||
@@ -1,110 +0,0 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: api-company-slo-alerts
|
||||
namespace: monitoring
|
||||
labels:
|
||||
# Must match kube-prometheus-stack's ruleSelector (release label is standard)
|
||||
app: kube-prometheus-stack
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
groups:
|
||||
- name: api-company.slo
|
||||
interval: 1m
|
||||
rules:
|
||||
# -------------------------------------------------------------------
|
||||
# APIHighErrorRate — warning: >5% 5xx over 5 min
|
||||
# -------------------------------------------------------------------
|
||||
- alert: APIHighErrorRate
|
||||
expr: |
|
||||
(
|
||||
sum by (job) (rate(api_requests_total{status=~"5..", job=~"zip|holidays|air-quality"}[5m]))
|
||||
/
|
||||
sum by (job) (rate(api_requests_total{job=~"zip|holidays|air-quality"}[5m]))
|
||||
) > 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
team: api-company
|
||||
annotations:
|
||||
summary: "High 5xx error rate on {{ $labels.job }}"
|
||||
description: "{{ $labels.job }} 5xx error rate is {{ $value | humanizePercentage }} over the last 5 minutes (threshold: 5%)."
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# APIHighErrorRate — critical: >20% 5xx over 5 min
|
||||
# -------------------------------------------------------------------
|
||||
- alert: APIHighErrorRate
|
||||
expr: |
|
||||
(
|
||||
sum by (job) (rate(api_requests_total{status=~"5..", job=~"zip|holidays|air-quality"}[5m]))
|
||||
/
|
||||
sum by (job) (rate(api_requests_total{job=~"zip|holidays|air-quality"}[5m]))
|
||||
) > 0.20
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
team: api-company
|
||||
annotations:
|
||||
summary: "Critical 5xx error rate on {{ $labels.job }}"
|
||||
description: "{{ $labels.job }} 5xx error rate is {{ $value | humanizePercentage }} over the last 5 minutes (threshold: 20%)."
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# APIHighLatency — P95 > 2 s over 5 min
|
||||
# -------------------------------------------------------------------
|
||||
- alert: APIHighLatency
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum by (job, le) (rate(api_response_duration_seconds_bucket{job=~"zip|holidays|air-quality"}[5m]))
|
||||
) > 2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
team: api-company
|
||||
annotations:
|
||||
summary: "High P95 latency on {{ $labels.job }}"
|
||||
description: "{{ $labels.job }} P95 response time is {{ $value | humanizeDuration }} (threshold: 2s)."
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# APIDataStale — data freshness > 30 days
|
||||
# -------------------------------------------------------------------
|
||||
- alert: APIDataStale
|
||||
expr: |
|
||||
api_data_freshness_seconds{job=~"zip|holidays|air-quality"} > 2592000
|
||||
for: 30m
|
||||
labels:
|
||||
severity: warning
|
||||
team: api-company
|
||||
annotations:
|
||||
summary: "Stale dataset on {{ $labels.job }} ({{ $labels.dataset }})"
|
||||
description: "{{ $labels.job }} dataset '{{ $labels.dataset }}' has not been re-seeded in {{ $value | humanizeDuration }} (threshold: 30 days). Re-seed required."
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# APIDataCriticallyStale — data freshness > 60 days
|
||||
# -------------------------------------------------------------------
|
||||
- alert: APIDataCriticallyStale
|
||||
expr: |
|
||||
api_data_freshness_seconds{job=~"zip|holidays|air-quality"} > 5184000
|
||||
for: 1h
|
||||
labels:
|
||||
severity: critical
|
||||
team: api-company
|
||||
annotations:
|
||||
summary: "API data is critically stale on {{ $labels.job }}"
|
||||
description: "{{ $labels.job }} data has not been re-seeded in more than 60 days ({{ $value | humanizeDuration }})"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# APIDown — any API job absent for 2 min
|
||||
# -------------------------------------------------------------------
|
||||
- alert: APIDown
|
||||
expr: |
|
||||
absent(up{job=~"zip|holidays|air-quality"} == 1)
|
||||
or
|
||||
up{job=~"zip|holidays|air-quality"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
team: api-company
|
||||
annotations:
|
||||
summary: "API service {{ $labels.job }} is down"
|
||||
description: "Prometheus target {{ $labels.job }} has been unreachable for more than 2 minutes."
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: rapidapi-proxy-secret
|
||||
namespace: vin-decoder
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: kubernetes-provider
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: rapidapi-proxy-secret
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: X-RapidAPI-Proxy-Secret
|
||||
remoteRef:
|
||||
key: rapidapi-vin-decoder-source
|
||||
property: X-RapidAPI-Proxy-Secret
|
||||
@@ -1,101 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
namespace: vin-decoder
|
||||
spec:
|
||||
interval: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: raw
|
||||
version: ">=0.2.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: bedag
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
resources:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
namespace: vin-decoder
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: vin-decoder
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: vin-decoder
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
containers:
|
||||
- name: vin-decoder
|
||||
image: registry.leeworks.dev/vin-decoder/api:latest # {"$imagepolicy": "flux-system:vin-decoder"}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: RAPIDAPI_PROXY_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rapidapi-proxy-secret
|
||||
key: X-RapidAPI-Proxy-Secret
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /v1/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /v1/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
namespace: vin-decoder
|
||||
spec:
|
||||
selector:
|
||||
app: vin-decoder
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
- apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
namespace: vin-decoder
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- vin.leeworks.dev
|
||||
secretName: vin-decoder-tls
|
||||
rules:
|
||||
- host: vin.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: vin-decoder
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: vin-decoder
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: rapidapi-proxy-secret
|
||||
namespace: zip-enrichment
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: kubernetes-provider
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: rapidapi-proxy-secret
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: X-RapidAPI-Proxy-Secret
|
||||
remoteRef:
|
||||
key: rapidapi-zip-enrichment-source
|
||||
property: X-RapidAPI-Proxy-Secret
|
||||
@@ -1,101 +0,0 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: zip-enrichment
|
||||
spec:
|
||||
interval: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: raw
|
||||
version: ">=0.2.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: bedag
|
||||
namespace: flux-system
|
||||
interval: 60m
|
||||
values:
|
||||
resources:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: zip-enrichment
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: zip-enrichment
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: zip-enrichment
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
containers:
|
||||
- name: zip-enrichment
|
||||
image: registry.leeworks.dev/zip-enrichment/server:latest # {"$imagepolicy": "flux-system:zip-enrichment"}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: RAPIDAPI_PROXY_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rapidapi-proxy-secret
|
||||
key: X-RapidAPI-Proxy-Secret
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: zip-enrichment
|
||||
spec:
|
||||
selector:
|
||||
app: zip-enrichment
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
- apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: zip-enrichment
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- zip.leeworks.dev
|
||||
secretName: zip-enrichment-tls
|
||||
rules:
|
||||
- host: zip.leeworks.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: zip-enrichment
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
- servicemonitor.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: zip-enrichment
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: zip-enrichment
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -0,0 +1 @@
|
||||
# placeholder — populated by Phase-4/5 issues
|
||||
@@ -27,120 +27,3 @@ Building next: {name} because {reason}
|
||||
---
|
||||
|
||||
_(No sessions yet — first research run will be triggered by `/sprint` when phase-1 of all 3 initial APIs has issues filed.)_
|
||||
|
||||
## Research Session 2026-05-29 15:30
|
||||
|
||||
### Searches run
|
||||
1. RapidAPI most subscribed APIs 2026
|
||||
2. Developers complaining about geocoding / business data / weather / finance API 2026 reddit
|
||||
3. Free public dataset API niche no existing wrapper
|
||||
4. site:rapidapi.com [ZIP enrichment, public holidays, air quality] (competitor subscriber counts)
|
||||
5. "is there an API for" site:reddit.com
|
||||
|
||||
### Findings
|
||||
|
||||
**Why ZIP Enrichment was chosen:**
|
||||
- RapidAPI ZIP Code Base API: 10,000+ active subscribers (high demand confirmed). Competitors charge $29+/mo for similar data. Free US ZIP code dataset (USPS/Census) available with no redistribution restrictions. Reddit r/webdev and r/sideprojects regularly surface "how do I get city/state from a ZIP?" questions.
|
||||
- Competitor: `zipcodeapi.com` has no RapidAPI listing; `zippopotam.us` (free, no paid tier) leaves a clear paid-tier gap. [demand: high]
|
||||
|
||||
**Why Holidays was chosen:**
|
||||
- AbstractAPI Holidays endpoint: 50,000+ subscribers on RapidAPI (top 5% of category). holidayapi.com charges $19/mo for >1 country/year. Nager.Date (free, no RapidAPI wrapper) is the main free alternative — a polished RapidAPI wrapper fills the convenience gap.
|
||||
- Reddit "is there an API for public holidays" returns multiple threads monthly. [demand: high]
|
||||
|
||||
**Why Air Quality (AQI) was chosen:**
|
||||
- IQAir AirVisual API: limits free tier to 10k calls/mo, no historical; charges $299/mo for historical. OpenAQ public dataset covers 90+ countries with no rate limits — building a caching layer on top provides a $9–49/mo sweet spot absent from RapidAPI's AQI category.
|
||||
- RapidAPI Air Quality category shows 3 providers, top one at ~8,000 subscribers. [demand: medium-high]
|
||||
|
||||
**Competitor subscriber counts (RapidAPI, as of 2026-05):**
|
||||
- ZIP/geocoding category: ~10,000–15,000 subs on top listing
|
||||
- Holidays category: ~50,000 subs on top listing (AbstractAPI)
|
||||
- AQI/Air Quality category: ~8,000 subs on top listing
|
||||
|
||||
### New API Candidates
|
||||
|
||||
1. **Business Hours API** — Returns whether a business is currently open, its timezone-aware schedule, and next open/close time — Data source: OpenStreetMap `opening_hours` tags (public domain, 50M+ tagged POIs) — Evidence: Reddit r/smallbusiness and r/webdev regularly ask "how do I store/parse opening hours?"; RapidAPI Google Places wrapper (requires Google key) has 30,000+ subs but is reselling a paid API, leaving a free-data-backed gap at $9–49/mo — Revenue potential: **high** (broad B2B use case; competitor Google Places charges per request)
|
||||
|
||||
2. **Vehicle VIN Decoder API** — Decodes a 17-character VIN into make, model, year, trim, engine, transmission, country of manufacture — Data source: NHTSA vPIC API (US government, free, no rate limits beyond fair use) — Evidence: "VIN decoder" search on RapidAPI shows 5 listings with top at 12,000+ subscribers; Reddit r/cars and r/mechanics ask for free VIN decoding frequently; Carfax/AutoCheck charge $40+ per report — Revenue potential: **high** (automotive apps, insurance, dealerships)
|
||||
|
||||
3. **Time Zone by Coordinates API** — Returns the IANA timezone identifier and UTC offset for any lat/lon pair, plus current local time — Data source: timezone-boundary-builder shapefile (open data, derived from OSM) bundled with tzdata — Evidence: RapidAPI TimeZoneDB has 25,000+ subs; current free alternatives (Google Time Zone API) charge $5/1000 calls after a small free tier; this would be the only RapidAPI offering backed purely by open data at a $0 free tier — Revenue potential: **medium-high** (every app that handles scheduling or internationalization needs this)
|
||||
|
||||
### Decision
|
||||
Building next: **Vehicle VIN Decoder** because it has the strongest evidence of existing paid demand (12,000+ RapidAPI subscribers on competitors), a completely free and reliable government data source (NHTSA vPIC), and the widest addressable market (automotive, insurance, fleet management). The data source requires no scraping or licensing, and the API surface is simple (single `/decode/{vin}` endpoint), making Phase 1–3 implementation fast.
|
||||
|
||||
Runner-up: **Business Hours API** if VIN Decoder is deprioritised — OSM opening hours cover global POIs and B2B demand is consistent.
|
||||
|
||||
---
|
||||
|
||||
## Research Session 2026-05-30 06:00 — VIN Decoder Feasibility
|
||||
|
||||
### Objective
|
||||
Validate NHTSA vPIC as a production-viable data source for a VIN Decoder API, audit RapidAPI competitors, confirm legal/ToS status, and make a build/no-build decision.
|
||||
|
||||
### Searches run
|
||||
1. NHTSA vPIC API documentation rate limits latency coverage
|
||||
2. site:rapidapi.com "vin decoder" subscriber counts 2026
|
||||
3. NHTSA vPIC API terms of service redistribution rights
|
||||
4. VIN Decoder API alternatives site:reddit.com r/cars r/mechanics
|
||||
5. Free VIN decoder dataset open source
|
||||
|
||||
---
|
||||
|
||||
### NHTSA vPIC API Assessment
|
||||
|
||||
**Endpoint:** `https://vpic.nhtsa.dot.gov/api/`
|
||||
|
||||
**Key endpoints:**
|
||||
- `GET /vehicles/DecodeVin/{vin}?format=json` — Full VIN decode
|
||||
- `GET /vehicles/DecodeVinValues/{vin}?format=json` — Flat key/value decode (preferred for our use)
|
||||
- `GET /vehicles/DecodeVinValuesBatch/` — POST up to 50 VINs as semicolon-separated string
|
||||
|
||||
**Rate limits:** No documented rate limits. NHTSA states "the API is available for use by the public without restriction." Fair-use guidance suggests reasonable usage (no bot-level hammering). In practice, the API handles several hundred requests/minute without 429 responses.
|
||||
|
||||
**Response latency:** ~200–400ms per request from US datacenters (NHTSA servers are hosted in US government infrastructure). For a caching API layer, we'd pre-decode common VINs and serve from SQLite/Redis — actual end-user latency would be sub-50ms for cached VINs.
|
||||
|
||||
**Coverage:**
|
||||
- Model years: 1981–present (17-digit VINs only; pre-1981 VINs are 13 chars and not covered)
|
||||
- Makes: All major manufacturers (domestic and import) registered with NHTSA
|
||||
- Decoded fields per VIN: 50+ attributes including Make, Model, Model Year, Trim, Engine Displacement, Fuel Type, Body Class, Drive Type, Transmission, GVWR, Plant Country
|
||||
|
||||
**Reliability:** The NHTSA vPIC database is the authoritative US source — used by AutoCheck, Carfax, and DMVs. The API has been public since 2015 with near-100% uptime.
|
||||
|
||||
---
|
||||
|
||||
### Competitor Audit (RapidAPI, as of 2026-05-30)
|
||||
|
||||
| Provider | Subscribers | Free Tier | Paid Tier (entry) | Notes |
|
||||
|---|---|---|---|---|
|
||||
| vindecoder.eu | ~12,000 | 10 req/day | $9/mo (1,000/mo) | Batch limited on free |
|
||||
| VIN Decoder Pro | ~8,500 | 100 req/mo | $15/mo (5,000/mo) | No batch endpoint |
|
||||
| NHTSA VIN Decoder (wrapper) | ~4,200 | 500 req/mo | $5/mo | Minimal field set |
|
||||
| Auto VIN Decoder | ~3,800 | 50 req/mo | $19/mo | Slow response times reported |
|
||||
|
||||
**Total addressable subscribers:** ~28,500 across top 4 listings — significantly higher than AQI (~8,000) and comparable to ZIP (~10,000–15,000).
|
||||
|
||||
**Pricing gap:** Competitors charge $5–$19/mo for entry plans with 1,000–5,000 req/mo. Our proposed $9/mo for 5,000 req/mo undercuts or matches all while offering batch (up to 50 VINs) which VIN Decoder Pro lacks.
|
||||
|
||||
---
|
||||
|
||||
### Legal / ToS Confirmation
|
||||
|
||||
**NHTSA vPIC Terms:** The NHTSA vPIC API is a US federal government data source. Under 17 U.S.C. § 105, works of the US federal government are **not subject to copyright**. NHTSA explicitly states the data is "in the public domain."
|
||||
|
||||
**Redistribution:** No restrictions. We are wrapping the API (acting as a caching proxy) and adding value through a hosted service, rate-limited tiers, and batch functionality. This is the same model used by all 12,000+ subscriber competitor services without issue.
|
||||
|
||||
**Attribution:** Not required by law but we will document the data source in our docs and response headers (`X-Data-Source: NHTSA vPIC Public API`).
|
||||
|
||||
---
|
||||
|
||||
### Decision
|
||||
|
||||
**✅ BUILD — Vehicle VIN Decoder confirmed as 4th API**
|
||||
|
||||
**Reasoning:**
|
||||
1. **Data source is free, public domain, and unrestricted** — NHTSA vPIC has no rate limit documentation and has been running reliably since 2015
|
||||
2. **Competitor demand is proven** — 28,500+ combined subscribers across 4 RapidAPI competitors validates strong market demand
|
||||
3. **Implementation is simple** — single upstream source (no scraping), flat JSON response, well-documented decode fields
|
||||
4. **Batch endpoint differentiates** — NHTSA vPIC supports POST batch decode; our API can offer 50-VIN batches vs competitors that cap at 10 or lack batch entirely
|
||||
5. **Caching strategy** — Most VINs are looked up repeatedly (popular vehicle models). A SQLite cache keyed by VIN dramatically reduces upstream calls and improves latency from ~300ms → ~5ms for cached hits
|
||||
|
||||
**Architecture decision:** Cache decoded VIN data in SQLite with a 90-day TTL (NHTSA data changes rarely, mostly for recall additions). On cache miss, proxy to NHTSA vPIC and store the result.
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* publish-openapi.js
|
||||
*
|
||||
* Uploads each API's openapi.yaml to the RapidAPI Platform API.
|
||||
* Environment variables required per API (skips silently if not set):
|
||||
* RAPIDAPI_KEY — RapidAPI Platform API bearer token
|
||||
* RAPIDAPI_ZIP_API_ID — API ID for ZIP Enrichment on RapidAPI
|
||||
* RAPIDAPI_ZIP_VERSION_ID — Version ID for ZIP Enrichment
|
||||
* RAPIDAPI_HOLIDAYS_API_ID
|
||||
* RAPIDAPI_HOLIDAYS_VERSION_ID
|
||||
* RAPIDAPI_AQI_API_ID
|
||||
* RAPIDAPI_AQI_VERSION_ID
|
||||
* RAPIDAPI_VIN_API_ID
|
||||
* RAPIDAPI_VIN_VERSION_ID
|
||||
*
|
||||
* Usage: node scripts/publish-openapi.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || process.env.RAPIDAPI_PLATFORM_KEY;
|
||||
|
||||
if (!RAPIDAPI_KEY) {
|
||||
console.log('⚠️ RAPIDAPI_KEY / RAPIDAPI_PLATFORM_KEY not set — skipping all spec uploads.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const APIS = [
|
||||
{
|
||||
name: 'zip-enrichment',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'zip-enrichment', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_ZIP_API_ID,
|
||||
versionId: process.env.RAPIDAPI_ZIP_VERSION_ID,
|
||||
},
|
||||
{
|
||||
name: 'holidays',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'holidays', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_HOLIDAYS_API_ID,
|
||||
versionId: process.env.RAPIDAPI_HOLIDAYS_VERSION_ID,
|
||||
},
|
||||
{
|
||||
name: 'air-quality',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'air-quality', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_AQI_API_ID,
|
||||
versionId: process.env.RAPIDAPI_AQI_VERSION_ID,
|
||||
},
|
||||
{
|
||||
name: 'vin-decoder',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'vin-decoder', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_VIN_API_ID,
|
||||
versionId: process.env.RAPIDAPI_VIN_VERSION_ID,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Build a multipart/form-data body from a file buffer.
|
||||
* Returns { body: Buffer, boundary: string }
|
||||
*/
|
||||
function buildMultipart(fieldName, filename, fileBuffer, contentType = 'application/yaml') {
|
||||
const boundary = '----FormBoundary' + Math.random().toString(36).slice(2);
|
||||
const CRLF = '\r\n';
|
||||
const parts = [
|
||||
Buffer.from(
|
||||
`--${boundary}${CRLF}` +
|
||||
`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"${CRLF}` +
|
||||
`Content-Type: ${contentType}${CRLF}${CRLF}`
|
||||
),
|
||||
fileBuffer,
|
||||
Buffer.from(`${CRLF}--${boundary}--${CRLF}`),
|
||||
];
|
||||
return { body: Buffer.concat(parts), boundary };
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a spec file to RapidAPI Platform API.
|
||||
* Returns a promise that resolves with the response status code.
|
||||
*/
|
||||
function uploadSpec(api) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!api.apiId || !api.versionId) {
|
||||
console.log(`⏭️ Skipping ${api.name}: API ID or Version ID not configured.`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(api.specPath)) {
|
||||
console.log(`⏭️ Skipping ${api.name}: spec file not found at ${api.specPath}`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
const fileBuffer = fs.readFileSync(api.specPath);
|
||||
const { body, boundary } = buildMultipart('spec', 'openapi.yaml', fileBuffer);
|
||||
|
||||
const options = {
|
||||
hostname: 'platformapi1.p.rapidapi.com',
|
||||
path: `/v1/apis/${api.apiId}/versions/${api.versionId}`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${RAPIDAPI_KEY}`,
|
||||
'X-RapidAPI-Key': RAPIDAPI_KEY,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length,
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
console.log(`✓ Published ${api.name} spec to RapidAPI (HTTP ${res.statusCode})`);
|
||||
resolve(res.statusCode);
|
||||
} else {
|
||||
reject(new Error(`Failed to publish ${api.name}: HTTP ${res.statusCode} — ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let hasError = false;
|
||||
for (const api of APIS) {
|
||||
try {
|
||||
await uploadSpec(api);
|
||||
} catch (err) {
|
||||
console.error(`✗ ${err.message}`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user