Flows

Storefront Customer Auth Flow

Customer login and OTP verification on the storefront.

Storefront Customer Authentication & Order History Flow

Feature area: Public Storefront / Buyer-Facing Auth
Last updated: 2026-06-12


Overview

End-users (buyers) who visit a store's public website can now create an account and log in. Once authenticated they can view their complete order history for that store and track individual orders — without any merchant credentials.

Customer accounts are per-store: the same phone number can register independently on each store. Authentication uses phone + password with OTP verification (via the existing WhatsApp/SMS provider) for registration and password reset.

A separate JWT authentication scheme (StorefrontCustomerBearer) is used so merchant tokens cannot access customer endpoints and vice versa.


Auth Scheme Isolation

SchemeAudienceWho uses itEndpoints
BearerSendyClientStore owners, staff, drivers/api/v1/store/…, /api/v1/admin/…
McpBearerOAuth MCP audienceMCP OAuth clients/api/v1/mcp/…
StorefrontCustomerBearersendy-storefront-customersStore buyers/api/v1/public/storefronts/{slug}/me/…

Both merchant and customer JWTs are signed with the same JwtSettings:Secret but carry different aud claims — a merchant token is rejected by the storefront customer validator and vice versa.


Flow 1 — Registration

Customer (browser)                        API                              OTP Provider
       │                                   │                                    │
       ├── POST auth/otp/send ────────────►│                                    │
       │   { phone, purpose: Registration} │                                    │
       │                                   ├── validate store slug              │
       │                                   ├── validate Iraqi mobile format     │
       │                                   ├── OtpVerifications.Add()           │
       │                                   ├── SaveChangesAsync()               │
       │                                   ├── SendVerificationCodeAsync() ─────►│
       │◄── 200 "Verification code sent" ──┤                     SMS/WhatsApp  │
       │                                   │                                    │
       │   (customer enters 6-digit code)  │                                    │
       │                                   │                                    │
       ├── POST auth/register ────────────►│                                    │
       │   { phone, otpCode,               │                                    │
       │     password, fullName? }         │                                    │
       │                                   ├── verify OTP (hash match + expiry) │
       │                                   ├── check phone uniqueness per store │
       │                                   ├── StorefrontCustomers.Add()        │
       │                                   ├── SaveChangesAsync()               │
       │                                   ├── link historical orders           │
       │                                   │   (CustomerPhone == phone,         │
       │                                   │    StoreId == store,               │
       │                                   │    StorefrontCustomerId == null)   │
       │                                   ├── generate JWT (StorefrontCustomerBearer)
       │                                   ├── create refresh token             │
       │◄── 200 { token, refreshToken,     │                                    │
       │          tokenExpiresAt,          │                                    │
       │          refreshTokenExpiresAt,   │                                    │
       │          customer }               │                                    │

Endpoint: POST /api/v1/public/storefronts/{slug}/auth/register
On success: account created, all prior guest orders with the same phone are auto-linked, JWT issued.


Flow 2 — Login

Customer (browser)                        API
       │                                   │
       ├── POST auth/login ───────────────►│
       │   { phone, password }             │
       │                                   ├── resolve store by slug
       │                                   ├── normalize phone (Iraqi format)
       │                                   ├── lookup StorefrontCustomer
       │                                   │   (StoreId + Phone + !IsDeleted)
       │                                   ├── PasswordService.VerifyPassword()
       │                                   ├── check IsActive
       │                                   ├── update LastLoginAt
       │                                   ├── generate JWT
       │                                   ├── create refresh token
       │◄── 200 { token, refreshToken, … } │
       │                                   │
       │   (token expires)                 │
       │                                   │
       ├── POST auth/refresh ─────────────►│
       │   { refreshToken }                │
       │                                   ├── hash lookup in StorefrontCustomerRefreshTokens
       │                                   ├── revoke old token
       │                                   ├── issue new JWT + new refresh token
       │◄── 200 { token, refreshToken, … } │

Login endpoint: POST /api/v1/public/storefronts/{slug}/auth/login
Refresh endpoint: POST /api/v1/public/storefronts/{slug}/auth/refresh
Logout endpoint: POST /api/v1/public/storefronts/{slug}/auth/logout — revokes the refresh token.


Flow 3 — Password Reset

Customer (browser)                        API
       │                                   │
       ├── POST auth/otp/send ────────────►│
       │   { phone,                        │
       │     purpose: PasswordReset }      │
       │                                   ├── store OTP (purpose = StorefrontCustomerPasswordReset)
       │◄── 200 "Verification code sent" ──┤
       │                                   │
       ├── POST auth/password/reset ───────►│
       │   { phone, otpCode, newPassword } │
       │                                   ├── verify OTP
       │                                   ├── lookup customer by store + phone
       │                                   ├── PasswordHash = HashPassword(newPassword)
       │                                   ├── SaveChangesAsync()
       │◄── 200 "Password reset" ──────────┤

Endpoint: POST /api/v1/public/storefronts/{slug}/auth/password/reset


Flow 4 — Order History

Requires a valid StorefrontCustomerBearer JWT in the Authorization header.

Customer (browser)                        API                              DB
       │                                   │                                │
       ├── GET me/orders                   │                                │
       │   Authorization: Bearer {token} ─►│                                │
       │                                   ├── validate StorefrontCustomerBearer JWT
       │                                   ├── extract sub (customerId) + store_id
       │                                   ├── Orders WHERE                 │
       │                                   │   StorefrontCustomerId == sub  │
       │                                   │   AND StoreId == store_id ─────►│
       │◄── 200 { items, totalCount,       │◄───────────────────────────────┤
       │          pageIndex, totalPages }  │                                │
       │                                   │                                │
       ├── GET me/orders/{publicId} ──────►│                                │
       │                                   ├── Order WHERE publicId         │
       │                                   │   AND StorefrontCustomerId     │
       │                                   │   AND StoreId ─────────────────►│
       │                                   │   include Items + StatusHistory │
       │◄── 200 { order detail }           │◄───────────────────────────────┤

List endpoint: GET /api/v1/public/storefronts/{slug}/me/orders?page=1&pageSize=20
Detail endpoint: GET /api/v1/public/storefronts/{slug}/me/orders/{publicId}

Order detail includes: status, items (SKU, name, qty, price), full status history, payment info, delivery timestamps.


Flow 5 — Authenticated Order Placement

When a logged-in customer places a new order the order is automatically linked to their account — no extra step required.

Customer (browser)                        API
       │                                   │
       ├── POST /{slug}/orders            │
       │   Authorization: Bearer {token}  │
       │   { …order body… } ─────────────►│
       │                                   ├── AuthenticateAsync("StorefrontCustomerBearer")
       │                                   ├── if succeeded → extract customerId from sub
       │                                   ├── CreateOrderAsync(…, storefrontCustomerId)
       │                                   ├── Order.StorefrontCustomerId = customerId
       │◄── 200 { publicId, … } ──────────┤

Guest (unauthenticated) order placement continues to work exactly as before — StorefrontCustomerId is left null.


On first registration all prior guest orders for the store that match the customer's phone are linked in bulk:

SELECT Orders WHERE StoreId = @storeId
                AND CustomerPhone = @phone
                AND StorefrontCustomerId IS NULLSET StorefrontCustomerId = @newCustomerId

This means the customer immediately sees their complete history after registering, even if they previously ordered as a guest.


JWT Token Claims

ClaimValue
subStorefrontCustomer.Id (Guid)
store_idStore.Id (Guid)
phoneNormalized Iraqi mobile (9647XXXXXXXXX)
role"storefront_customer"
aud"sendy-storefront-customers"
issJwtSettings:Issuer
expJwtSettings:ExpiryMinutes from now

API Endpoint Summary

All routes begin with /api/v1/public/storefronts/{slug}/.

MethodRouteAuth requiredDescription
POSTauth/otp/sendNoSend OTP (registration or password reset)
POSTauth/registerNoRegister with phone + OTP + password
POSTauth/loginNoLogin → JWT + refresh token
POSTauth/password/resetNoReset password via OTP
POSTauth/refreshNoRotate refresh token
POSTauth/logoutYesRevoke refresh token
GETme/ordersYesPaginated order history
GETme/orders/{publicId}YesOrder detail + status history

Source References

LayerFile
Domain entitySendy.Domain/Entities/Storefront/StorefrontCustomer.cs
Refresh token entitySendy.Domain/Entities/Storefront/StorefrontCustomerRefreshToken.cs
OTP purposesSendy.Domain/Enums/Identity/OtpPurpose.cs
Auth service interfaceSendy.Application/Interfaces/Services/Public/IStorefrontCustomerAuthService.cs
Auth service implementationSendy.Application/Services/Public/StorefrontCustomerAuthService.cs
Order history serviceSendy.Application/Services/Public/StorefrontCustomerOrderHistoryService.cs
JWT token generatorSendy.Infrastructure/Services/Auth/StorefrontCustomerJwtTokenGenerator.cs
Auth constantsSendy.Infrastructure/Constants/AuthConstants.cs
Auth controllerSendy.Api/Controllers/V1/Public/StorefrontCustomerAuthController.cs
Orders controllerSendy.Api/Controllers/V1/Public/StorefrontCustomerMeController.cs
Scheme registrationSendy.Api/Hosting/WebApplicationBuilderExtensions.cs
EF configurationSendy.Infrastructure/Data/EntityConfigurations/Storefront/StorefrontCustomerConfiguration.cs
MigrationSendy.Infrastructure/Migrations/…AddStorefrontCustomerAuth.cs

Source: DOCS/flows/storefront-customer-auth-flow.md