Skip to content

P1 Catalog + Admin Core: catalog API, onboarding/staff backend, merchant admin UI + DoD e2e - #2

Merged
helgarpalmieri55 merged 47 commits into
mainfrom
claude/nueva-plataforma-superpowers-o3r31f
Jul 25, 2026
Merged

helgarpalmieri55 merged 47 commits into
mainfrom
claude/nueva-plataforma-superpowers-o3r31f

Conversation

@helgarpalmieri55

Copy link
Copy Markdown
Owner

Summary

Phase P1 of Ventia per docs/SPEC.md §11 (M1, M2, M8 partial), built with the superpowers workflow across three sub-plans, each with per-task TDD implementation + independent review, phase reviews, and a final whole-branch review ("Ready to merge: Yes").

P1a — Catalog API backend

  • MinIO dev storage (S3 API, R2-compatible), presigned product-image uploads with security gates (session-derived tenant, key-prefix check, HeadObject size/type verification with delete-on-violation).
  • Admin session/role guards (/v1/admin/*, narrowed AdminSessionContext), categories/products CRUD with plan limits (402 typed errors), slug/SKU semantics (@@unique(tenantId, sku)), archive-not-delete with un-archive limit accounting.
  • Variants (≤3 options + Product.options migration), stock adjustments with InventoryMovement ledger (race-safe atomic SQL), image cap enforced under concurrency (FOR UPDATE).
  • CSV import: streaming parse with Spanish row errors, dry-run preview, transactional commit (manual RLS-scoped transaction), PATCH-like update semantics (blank cells never reset fields), 500 rows ≈ 900 ms vs 60 s AC.
  • Hardening wave from the phase review: cross-tenant categoryId rejection, raw-query blocking already in P0's client extended by revoked ventia_app grants on system tables, shadow-DB-safe migrations (prisma migrate dev restored).

P1b — Onboarding + Staff + lifecycle backend

  • Tenant provisioning (single transaction: Tenant/limits/domain/owner membership; membership-based ALREADY_HAS_TENANT gate incl. platform_admin), resumable wizard state, live launch checklist.
  • Mailer abstraction (console transport in dev) + better-auth email verification (verify link lands on the admin origin).
  • Owner-only settings (store info, theme with fixed 5-font catalog, COD toggle) — M8 AC (staff blocked server-side) tested on every route.
  • Staff invites: 192-bit tokens stored sha256-hashed, seat limits, opaque INVITE_INVALID, transactional accept; invite email links to the real accept page.
  • Launch (draft → live, verification-gated) and suspended semantics (admin GET-only, storefront unavailable message — strict 503 documented as P2 deviation).
  • Tenant slug validation (format regex + reserved-label blocklist) since slugs become verified domains.

P1c — Merchant admin UI (es-CO)

  • packages/ui: Tailwind 4 + shadcn-style primitives (native-<dialog> accessible modal, ARIA-wired FormField).
  • Admin app: same-origin /api proxy (better-auth cookies without CORS), typed fetch client with es-CO error mapping verified against actual API codes.
  • Auth pages + role-aware shell, onboarding wizard (revisit-safe: saved theme/payments pre-filled and kept fresh), products list/form (variants editor, presigned uploads, stock adjuster), categories, CSV importer (template → dry-run → commit), staff + settings pages (tab state preserved).
  • Playwright DoD e2e (local run; CI e2e deferred to P6): owner journey (signup → wizard → 10-product CSV import → email verification → launch → live storefront on subdomain) + staff journey (invite email link → accept → role-restricted access). Green 3× including cold cache; it caught and fixed two real integration bugs (better-auth trustedOrigins for the admin origin; turbo typecheck/build race).

Verification

  • pnpm turbo run lint typecheck build — 21/21; pnpm turbo run test — 8/8 packages: api 138, admin 93, core 17, db 16, storefront 2.
  • bash scripts/e2e.sh — 2/2 specs (the P1 DoD path end-to-end through Caddy).
  • 24 task-scoped reviews + P1a phase review + final whole-branch review with pre-merge fix wave; every Critical/Important finding closed with regression tests.
  • Documented deviations: suspended storefront serves HTTP 200 message (503 in P2), archived-404 on storefront in P2, ADMIN_URL required in production (README Production notes).
  • Backlog carried explicitly to P2/P6 in the progress ledger (mailer provider, real 503, observability, ESM runtime packaging, etc.).

Docs

  • Design: docs/superpowers/specs/2026-07-23-p1-catalog-admin-design.md (with amendments)
  • Plans: docs/superpowers/plans/2026-07-23-p1a-catalog-api.md, 2026-07-24-p1b-onboarding-staff.md, 2026-07-24-p1c-admin-ui.md

🤖 Generated with Claude Code

https://claude.ai/code/session_012tUfx41TMxana567RboGMb


Generated by Claude Code

claude added 30 commits July 23, 2026 21:19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds /v1/admin/products (list/create/get/patch/archive) alongside a
reusable assertProductLimit() plan-limit check, following the same
tenantDb + parseOr400 + writeAudit patterns established for categories.

- products.service.ts: search/status/pagination-aware list, slug
  auto-derivation with tenant-unique dedup (base, base-2, ... base-20,
  then 409 SLUG_TAKEN), categoryIds connect-on-create and
  replace-on-patch via the ProductCategory join, and archive (soft
  delete: status -> archived, not a hard delete).
- plan-limits.ts: assertProductLimit() counts non-archived products
  against tenant_limits.productsMax (unlimited when no limits row
  exists), throwing 402 PLAN_LIMIT_EXCEEDED at/over the cap. Exported
  for reuse by the Task 8 CSV bulk-import commit path.
- Both roles (owner/staff) can manage products, matching the
  categories controller's guard configuration.

Test coverage in products.test.ts: create defaults, slug dedup, list
search/status/pagination, plan limit enforcement, archive semantics
(still readable, excluded from active list, frees up the limit),
staff-role access, category connect/replace, cross-tenant 404s, and
audit rows for create/update/archive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds PUT /v1/admin/products/:id/variants (atomic variant-set replace +
product.options via the manual RLS transaction escape), image presign/
confirm/delete endpoints wired to StorageService with S3 HeadObject/
DeleteObject validation gates, and POST /v1/admin/products/:id/stock
with InventoryMovement rows written in the same transaction as the
stock update. Also adds the product_options migration and a PATCH
same-slug regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Self-review surfaced two concurrency bugs: stock adjust computed the
new stock value from a read taken before its transaction and wrote it
as an absolute SET, so two concurrent adjustments could both pass the
floor check against the same stale read and silently lose one of them;
and the 8-image cap was only checked at presign time, so two concurrent
confirms could both observe count<8 and both land, exceeding the cap.

Fixes both with a single atomic conditional statement (UPDATE ... WHERE
stock + delta >= 0 RETURNING stock) for stock, and a SELECT ... FOR
UPDATE row lock serializing the count-check-then-insert for images —
both inside the existing manual RLS transaction. Adds two regression
tests exercising concurrent requests against each.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Implements the P1a Task 8 / spec M2 CSV bulk-import acceptance criterion:
GET /v1/admin/import/template, POST .../dry-run, and POST .../commit, with
a pure csv-parser.ts (papaparse + a zod row schema in @ventia/core) that
produces Spanish (es-CO) row-level errors. Commit runs entirely inside one
manual RLS transaction (the ProductsService.update pattern) so a file with
any row error, or one that would exceed the plan's product limit, leaves
nothing persisted. assertProductLimit gained an additionalCount param so
the commit path can count only CREATE rows against the limit. 500-row
commit measured at ~900ms in this container, well under the 60s AC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ody limit

Addresses code review findings on the CSV import feature (Task 8):

- Commit's per-row update was overwriting every column with the CSV row's
  create-time defaults, so a blank cell on an update (e.g. no track_inventory
  value) reset that field instead of leaving it alone. csv-parser.ts now
  records which optional columns were actually provided (ParsedRow.provided),
  and the commit path uses that to build a PATCH-like update: only name and
  price_cents are unconditional; stock/status/track_inventory/tax_rate/
  description/images/categories are touched only when their column was
  non-blank, and slug is left alone (never re-derived from name) unless the
  slug column was explicit.
- Category resolution in the commit transaction now keys its cache by slug
  (via slugify(name), with a P2002-catch-and-refetch fallback and a
  deterministic categoria-N fallback for names that slugify to '') instead of
  by lowercased name — fixes a same-file "Café" vs "Cafe" pair collapsing to
  two categories that then collide on Category's unique (tenantId, slug)
  index and abort the whole commit.
- main.ts's express.json() body-size increase is now scoped to
  /v1/admin/import/* (mounted before the global default-limit parser, which
  body-parser skips re-running once a request's body is already parsed)
  instead of raising the limit for every route.
- Parser surfaces a file-level row:0 error for a structurally malformed CSV
  (papaparse's own tokenizer errors), and the commit audit row's entity label
  is 'CsvImport' rather than the generic 'Product'.

New/updated tests: PATCH-like update regression (blank cells preserved),
accent/case category-collision regression, malformed-CSV file-level error,
provided-flags assertions on the two existing defaulting tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Phase-review hardening wave (P1a):
- Reject cross-tenant/nonexistent categoryIds on product create and update
  with a typed 400 VALIDATION_FAILED, checked via a tenant-scoped count
  before the write instead of a foreign-key catch (create() previously
  500'd here). Duplicate categoryIds in one request are deduped first so
  they no longer trip the ProductCategory unique constraint and get
  misreported as SLUG_TAKEN.
- Discriminate a product's P2002 into SKU_TAKEN vs SLUG_TAKEN instead of
  always reporting SLUG_TAKEN. meta.target can't be used here: every write
  runs as the ventia_app RLS role, and Postgres omits the constraint name
  from unique-violation errors for roles without full table privileges
  (verified directly against this schema/role) — so the conflict type is
  determined with a targeted re-query instead.
- Add assertUuidOr404 and wire it into every :id/:imageId handler across
  products/categories/variants/images/stock, so a malformed uuid path
  param 404s like any other not-found id instead of 500ing on Prisma's
  P2023.
- Validate the image confirm key's suffix (`{uuid}.{ext}`) after the
  tenant/product prefix check, closing a path-traversal gap the prefix
  check alone didn't cover.
- Un-archiving a product via PATCH (status -> non-archived) now runs the
  plan-limit check before the write, since archived products don't count
  toward the limit.
- Add the missing M2 AC test: deleting a category with products attached
  returns 204 and leaves the products in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Phase-review hardening wave (P1a), continued:
- Remove `stock` from productUpdateSchema (kept on productInputSchema and
  the CSV row schema) so PATCH can no longer silently overwrite stock —
  POST /:id/stock and the CSV import path below are now the only ways to
  change it, both of which guarantee an InventoryMovement row.
- CSV commit: an UPDATE row that changes an existing product's stock now
  writes an InventoryMovement (reason csv_import) for the delta; CREATE
  rows still set a baseline with no movement.
- CSV commit/dry-run: a row that un-archives a product (explicit `status`
  column on a currently-archived product) now counts toward the plan
  limit alongside creates, matching PATCH's un-archive behavior.
- Raise the commit transaction's timeout to 60s to match the CSV AC
  budget instead of Prisma's 5s interactive-transaction default.
- resolveCategoryId: pre-check the `categoria-N` fallback slug (used for
  category names that slugify to '') against every slug already taken in
  the tenant, instead of attempting the create and trying to recover from
  a P2002 in the catch — a failed statement aborts the whole Postgres
  transaction, so that recovery could never actually run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ueness

- Wrap each REVOKE in the 20260723205801 migration in a
  `to_regclass(...) IS NOT NULL` guard so `prisma migrate dev`/`reset`'s
  shadow-DB replay can't fail if migration ordering ever puts a REVOKE
  ahead of its table. Verified via `prisma migrate diff --shadow-database-url`
  (a non-interactive shadow-DB replay of all migrations) and via a fresh
  Testcontainers run, both green. The already-applied dev DB was
  recreated from scratch (docker volume dropped, migrations re-deployed,
  reseeded) since editing an applied migration invalidates its checksum.
- Add `@@unique([tenantId, sku])` to Product (multiple NULLs still
  allowed). The migration was generated with `prisma migrate diff`
  against a real shadow database (this sandbox has no TTY, which
  `prisma migrate dev` requires) — the generated SQL is identical to
  what `migrate dev` would have produced, and it was applied to the dev
  DB via `prisma migrate deploy`.
- Product create/update now discriminate the resulting P2002 into
  SKU_TAKEN vs SLUG_TAKEN (see the previous commit); CSV import's
  in-file duplicate-sku handling is unchanged (already rejected per-file
  by the parser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds a Mailer interface + ConsoleMailer (MAILER DI token via MailerModule)
and wires better-auth's emailVerification.sendVerificationEmail through it,
so signup sends a Spanish verification email whose URL, when GET-ed, flips
user.emailVerified. GET /v1/admin/me now also returns emailVerified,
surfaced per-request rather than folded into AdminSessionContext.

Also caps the CSV category base-slug suffix loop at 20 (mirroring the
product path) with a Spanish row-level error, and completes a loose
AdminSessionContext test cast that was missing email/role.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds P1b Task 4: GET/PATCH/PUT /v1/admin/settings routes (store info,
theme, COD payments) gated to the owner role, delivering spec M8's
"staff cannot reach settings" acceptance criterion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds the StaffInvite model (+ RLS policy in a follow-up create-only
migration) and a staff/ module: owner-only invite/list/revoke/remove
endpoints under /v1/admin/staff, plus a public POST /v1/staff/accept
for a signed-in invitee to redeem their token. Seat checks count
accepted staff memberships and pending invites against
tenantLimits.staffSeats (absent row = unlimited). Invite tokens are
48 hex chars, stored only as a sha256 hash; the accept link doubles
as the credential, so email match is deliberately not enforced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds POST /v1/admin/launch (owner-only), reusing the onboarding
service's checklist computation: not-ready returns 422
LAUNCH_CHECKLIST_INCOMPLETE with per-item details, ready flips
tenant.status to 'live' and writes a tenant.launch audit row, and a
second call against an already-live tenant is idempotent (no
recompute, no duplicate audit).

Also lands suspended-tenant semantics: AdminSessionGuard now rejects
non-GET /v1/admin/* requests with 403 TENANT_SUSPENDED when the
session's tenant is suspended (one extra indexed PK read, skipped
entirely for GET traffic), and the storefront home page renders "Tienda
temporalmente no disponible" for a suspended tenant instead of the
strict 503 deferred to the P2 storefront rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Records the onboarding wizard, staff invite/accept, and launch-checklist
endpoints plus the console-transport mailer note, verified end-to-end via
the P1b Task 7 M1 acceptance smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
claude added 17 commits July 24, 2026 22:10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Sets up @ventia/ui as a real package: Tailwind 4 styles.css with CSS
variable theming (primary/background/foreground/radius per spec §5.4),
a cn() class-merge util, and dependency-free shadcn-style primitives
(Button, Input, Label, Card, Dialog, Select, Table, Badge, Alert,
Spinner, FormField).

Wires apps/admin onto it: Tailwind via @tailwindcss/postcss, next.config
/api rewrite to API_INTERNAL_URL, a typed apiFetch/ApiError client, and
an es-CO error-code-to-message mapper (lib/errors.ts) covering every
error code the API currently emits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
When BrandingStep or PaymentsStep submit successfully, propagate the
submitted values back into wizard's settings state so any later remount
pre-fills from fresh data rather than the stale initial snapshot. This
prevents silent overwrites of merchant edits on step revisits.

- Add mergeSavedSettings and mergeSavedPaymentsSettings helpers to
  settings-merge.ts with unit tests (6 tests covering theme replace,
  payments merge, null prev).
- Extend BrandingStep.onDone callback to carry submitted theme object.
- Extend PaymentsStep.onDone callback to carry submitted codEnabled bool.
- In wizard, add handleBrandingDone and handlePaymentsDone that merge
  settings before advancing to the next step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds apps/admin/app/(app)/productos (search, status filter, pagination,
es-CO empty state) and app/(app)/categorias (list + create/rename/delete
dialogs handling SLUG_TAKEN/VALIDATION_FAILED). New lib/format.ts::formatCOP
renders integer cents as no-decimal es-CO COP, built RED-first in
test/format.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Implements P1c Task 5: create/edit product pages under apps/admin.

- apps/admin/app/(app)/productos/nuevo/page.tsx: create form validated
  client-side against @ventia/core's productInputSchema before submit.
- apps/admin/app/(app)/productos/[id]/page.tsx + _components/: edit page
  with base fields (PATCH, stock omitted per productUpdateSchema), a
  variants editor (PUT /variants, replace-semantics warning), an image
  manager (presign -> raw-fetch PUT to the signed URL -> confirm -> delete),
  a stock adjuster (POST /stock, product or variant target), and an
  archive action (DELETE) with unarchive via the status select.
- lib/format.ts: pesosToCents/centsToPesos (RED first in test/format.test.ts).
- lib/variants.ts: buildVariantsPayload, filtering blank labels/rows and
  trimming (RED first in test/variants.test.ts).
- lib/validation.ts, lib/product-labels.ts: shared zod-issue mapping and
  es-CO label constants for the two product pages.
- packages/ui/src/button.tsx: adds an optional `href` prop (renders a
  styled <a> instead of <button>) per the T4 review rider, fixing the
  nested <a><Button/></a> in productos/page.tsx's call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds the equipo (staff invites/members/revoke/remove), aceptar-invitacion
(public invite-accept flow), and configuracion (tienda/marca/pagos tabs)
admin pages, plus a TDD'd inviteStatus() helper for the pending-invites
table. Also collapses the importar page's pre-commit section into a
success-only view with a reset button, per the T6 review rider.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
- Tienda tab: send category/contactPhone/description unconditionally (not
  only when truthy) so blanking a field actually persists — PATCH
  /settings/store merges storeInfo in place, so an omitted key silently
  left the old value untouched behind a false "saved" confirmation.
  contactEmail stays conditional (its schema rejects an empty string).
- Marca tab: wire VALIDATION_FAILED into per-field errors (logoUrl) and a
  colors-group alert, instead of collapsing to the generic
  "Revisa los campos marcados." message with nothing to point at.
- aceptar-invitacion: map a malformed token's VALIDATION_FAILED (fails the
  48-char check before the invite lookup runs) to the same INVITE_INVALID
  copy, rather than showing a validation message for a page with no form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
- Fix Finding 1: Keep all three tabs mounted with hidden attribute to preserve unsaved edits when switching tabs (instead of conditionally rendering which unmounts components)
- Fix Finding 2: Add faviconUrl field to ThemeFormState and themeToFormState; update MarcaTab and BrandingStep to include favicon URL input fields and preserve faviconUrl in theme payloads
- Extended theme-form tests to cover faviconUrl field (preserve when set, empty string default)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Playwright suite covering the full P1 Definition-of-Done: owner sign-up,
onboarding wizard (store info, branding, 10-product CSV import, COD),
email verification via the console-mailer log hook, launch, storefront
subdomain resolution, and a staff invite/accept flow hitting the
owner-only permission boundary. Also fixes two real bugs the e2e
surfaced: better-auth rejecting admin sign-up/sign-in with 403
INVALID_ORIGIN (missing trustedOrigins for the admin app's own origin),
and a turbo.json task-graph gap where typecheck could race a package's
own build and fail looking for generated Next.js route types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ant slugs

Staff-invite emails linked to a POST-only API endpoint that 404s on GET;
point them at the admin app's client-rendered accept page instead. The
verification email's callbackURL defaulted to an unqualified "/", which
303s a browser to the API's own origin instead of the admin app; the
sendVerificationEmail hook now forces it to ADMIN_URL. Tenant slugs were
otherwise-unvalidated free text that becomes a DNS subdomain label;
constrain them to slugify's own alphabet and block infrastructure-reserved
words (auto-derived collisions dedup-suffix instead of erroring).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
README: note the P1 suspended-storefront 200 (strict 503 deferred to
P2's storefront rebuild, alongside archived-products-404), and add a
Production notes section flagging that ADMIN_URL must be set in any real
deployment or sign-ins fail 403 INVALID_ORIGIN. Amend the P1 design doc's
storefront-503 sentence with the same P1 deviation note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
@helgarpalmieri55
helgarpalmieri55 merged commit 8a5f4b2 into main Jul 25, 2026
1 check passed
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.

2 participants