PLATFORM

Authentication

Authentication

Two separate identities, both JWT-based. Browsing the storefront stays public; anything belonging to a person is not.

Staff

POST /stores/:slug/auth/register   first user only -> becomes admin
POST /stores/:slug/auth/login      email + password
POST /stores/:slug/auth/google     Google ID token from the browser
GET  /stores/:slug/auth/me         who the token belongs to
POST /stores/:slug/auth/users      admins only; new users are staff

Users live in each store's own database, in a users table created with every other table when the store is provisioned — a store's users are its data. The control database holds only the store registry (slug -> database).

Roles are admin or staff. The first account created in a store becomes its admin, regardless of what is requested; registration then closes and further users are invited by an admin.

POST /stores/:slug/sync rebuilds an existing store's schema from the current entities, so stores provisioned before an entity was added pick it up. It uses TypeORM's synchronize, which is additive-safe but not a substitute for migrations in production.

A token carries its store's slug, and the guard rejects a token issued for a different store — otherwise swapping the X-Store-Slug header would let one shop's admin manage another.

Shoppers

Customers sign in to the storefront itself. The store comes from the request's tenant, so these routes never name it:

POST /auth/customer/register   email + password, creates the account
POST /auth/customer/login      email + password
POST /auth/customer/google     Google ID token from the browser
GET  /auth/customer/me         the signed-in shopper

Shoppers live in the same customers table the admin's Customers view reads — one record per person, whether they arrived by checkout or by registering. Checkout creates customers without credentials; registering later under that email claims the existing record, order history included, rather than creating a second row.

Keeping the two apart

Both tokens are signed with the same secret, so each carries a kind claim and each guard accepts only its own:

  • a shopper's token on /admin/*"That is a storefront session, not a staff one"
  • a staff token on /account/*"That is a staff session, not a shopper one"

Account data — addresses, payment methods, orders, preferences — is selected by the customer id inside the token, never by an id or email in the request. Order lookup by number is ownership-checked, and an order belonging to someone else reads as missing rather than forbidden, so responses cannot be used to discover which order numbers exist.

Passwords are bcrypt (12 rounds) and may be null for Google-only accounts. Set GOOGLE_CLIENT_ID (API) and NEXT_PUBLIC_GOOGLE_CLIENT_ID (admin) to enable Google sign-in; leaving them blank hides the button and the endpoint refuses cleanly.

JWT_SECRET must be at least 32 characters — the API refuses to start otherwise.