Skip to content

feat: zero-Azure local mode, single-instance guard, one-script provisioning - #56

Merged
markadams31 merged 2 commits into
mainfrom
feat/zero-azure-local-mode
Jul 5, 2026
Merged

feat: zero-Azure local mode, single-instance guard, one-script provisioning#56
markadams31 merged 2 commits into
mainfrom
feat/zero-azure-local-mode

Conversation

@markadams31

Copy link
Copy Markdown
Owner

Summary

Removes the biggest friction in this project: getting it running. Three deliverables:

1. Zero-Azure local mode

git clone … && cd autocrud && docker compose up --build
# → http://localhost:8000

That's the complete application — the production image (SPA + API, one origin) against a local SQL Server 2025 seeded from database/seed.sql (the worked-example schema: computed columns, temporal history, composite keys, rowversion, cross-schema FKs). Docker is the only prerequisite; no Azure subscription, no Entra sign-in, no configuration.

How it works:

  • DB_AUTH_MODE on the backend: entra (default — production posture, completely unchanged) or local (one SQL login for reflection and data). Local mode deliberately does not exercise the per-user-grants model; /me reports sa (local) so writes are attributable in the UI. Refused outright on App Service (WEBSITE_SITE_NAME guard in config.py) so a misconfigured deployment can never silently fall back to a shared SQL login.
  • A one-shot db-init service creates + seeds the database once; re-ups leave your data alone (docker compose down -v resets). .gitattributes pins *.sh/*.sql to LF so a Windows clone can't break the init script inside the Linux container.
  • Dev loop without Docker for the app: docker compose up db db-init, run the backend natively with the local-mode env (.env.example has the block), and AUTOCRUD_PROXY_TARGET=http://localhost:8000 npm run dev — two processes, no auth proxy. The vite proxy target is now configurable (default unchanged).
  • README gains a pitch-adjacent "Try it now — no Azure required" quickstart and a two-path Running locally section; CLAUDE.md records the commands.

2. Single-instance guard in Terraform

worker_count = 1 on the App Service plan, with the reasoning in-line: the schema snapshot, per-token engine cache, refresh throttle, and health cache are process-local, so a scaled-out deployment would serve a stale schema after /admin/refresh. Terraform now also reverts any manual portal scale-out on the next apply — the constraint is enforced, not remembered.

3. One-script hosted provisioning

pwsh infra/provision.ps1 [-Environment dev|prod] wraps the entire post-tfvars runbook: terraform init/apply → az acr build → app restart → seed (skipped when data exists; -ReseedDatabase to reset) → permissions.sql with the environment's group name substituted automatically → add yourself to the app-users group → write the local .env → print the app URL. Preflight catches the usual traps (not signed in, missing tfvars, legacy ODBC sqlcmd without Entra auth). The infra README now leads with the script; the step-by-step remains as reference.

Testing

  • Unit/API tiers: 461 passed (12 new tests: config gate incl. App Service refusal + credential requirement, tokenless local data path, entra-mode 401 unchanged, /me behaviours). ruff, pyright, tsc, oxlint, terraform fmt all clean.
  • Fresh-clone simulation (the actual first-time experience): cloned the repo into a scratch directory, ran the README quickstart verbatim, and drove the result in a real browser via Chrome DevTools — healthy in ~5 minutes including the image build, 13 tables in the sidebar, seeded rows rendering with FK labels, sa (local) identity badge, zero console errors. LF line endings verified to survive a Windows clone.
  • Contract checks through the compose stack: reflection (13 tables), CRUD round-trips, rowversion If-Match (fresh token updates; stale token → 409), audit triggers stamping the SQL login, seed-once on re-up, clean down -v reset.
  • Native dev-loop path: backend run natively in local mode against the compose database on a non-default port (Server=localhost,14330), confirming the documented two-process workflow.
  • Port knobs (AUTOCRUD_PORT, AUTOCRUD_SQL_PORT) exercised throughout (this machine's :8000 was occupied — which is exactly what they're for).

Not covered: infra/provision.ps1 has not been run against a real subscription in this session (it reorchestrates the commands the infra README already documents, and its SQL/az steps mirror what was previously copy-pasted); suggest exercising it on the next dev re-provision.

🤖 Generated with Claude Code

markadams31 and others added 2 commits July 5, 2026 12:42
…equired

`docker compose up --build` now runs the complete application (the production
image: SPA + API, one origin) against a local SQL Server 2025 seeded with the
worked-example schema (database/seed.sql), at http://localhost:8000. The only
prerequisite is Docker — no Azure subscription, no Entra sign-in, no config.

The backend gains DB_AUTH_MODE:
  entra (default)  the production posture, unchanged — Entra tokens end to
                   end, managed identity for reflection, per-user OBO tokens
                   for data.
  local            one SQL login (DB_USERNAME/DB_PASSWORD) for reflection AND
                   data — development/demo only. The per-user-grants model
                   deliberately does not apply; /me reports the SQL login so
                   writes are attributable. Refused outright on App Service
                   (WEBSITE_SITE_NAME guard in config.py) so a misconfigured
                   deployment can never silently fall back to a shared login.

Supporting pieces:
- db-init one-shot service creates + seeds the database once; re-ups leave
  your data alone ("docker compose down -v" resets the demo dataset).
- .gitattributes pins *.sh/*.sql to LF so a Windows clone (autocrlf) can't
  break the init script inside the Linux container.
- Vite proxy target is now AUTOCRUD_PROXY_TARGET (default unchanged :8001),
  so the local-mode dev loop points straight at the API — two processes, no
  auth proxy.
- .env.example documents both modes; README gains a "Try it now" quickstart
  and a two-path Running locally section; CLAUDE.md records the commands.

Verified end to end against the running stack: reflection (13 tables), CRUD
round-trips, rowversion If-Match (fresh token updates, stale token 409s),
audit triggers stamping the SQL login, /me identity, seed-once on re-up,
clean down -v reset. Unit tests cover the config gate (credentials required,
App Service refusal), the tokenless local data path, entra-mode 401s
unchanged, and the /me behaviours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ning

Single-instance guard: the app keeps process-local state that is only correct
on one instance — the reflected schema snapshot (a second instance would keep
serving a stale schema after POST /admin/refresh), the per-token engine cache,
the refresh throttle, and the health-probe cache. worker_count = 1 on the
service plan makes that constraint enforced rather than remembered: Terraform
now also reverts any manual portal scale-out on the next apply.

infra/provision.ps1 collapses the post-apply runbook (terraform init/apply,
az acr build, app restart, database seed + permission grants, adding yourself
to the app-users group, writing the local .env) into one idempotent command:

  pwsh infra/provision.ps1 [-Environment dev|prod] [-ReseedDatabase] [-SkipTerraform]

The demo seed is skipped when the database already has data (seed.sql is
destructive by design), permissions.sql gets the environment's group name
substituted automatically, and preflight checks catch the usual traps (not
signed in, missing tfvars, legacy ODBC sqlcmd without Entra auth). The infra
README now leads with the script and keeps the step-by-step as reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@markadams31
markadams31 merged commit e074b5c into main Jul 5, 2026
20 checks passed
@markadams31
markadams31 deleted the feat/zero-azure-local-mode branch July 5, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant