Customers & accounts
Invite Links
Generated from docs/plan/core/invite-links/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Invite Links generates one-time URL tokens that merchants send to new staff, who click the link to set their password and complete account setup without needing a temporary password.
The problem
Merchants can’t easily onboard new team members; they must manually create accounts for each person.
What it does
- Generate invite link token (URL-safe, 32+ random bytes, hashed in DB)
- Email invite link to staff
- Invite link valid 7 days (configurable)
- Resend invite (new token, old becomes invalid)
- Accepting invite: click link, set password, agree to terms, account activated
- Show role and permissions before acceptance
- Resend button: new link and re-email
- Prevent double-accept: token consumed on first use
- Cancel invite: revoke before user accepts
- Audit log: created, sent, accepted, revoked, expired
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Bulk invite (CSV upload of 100 emails) — single invite flow
- Invite with SSO (Slack sign-in) — email and password only
- Conditional invite (only if user doesn’t exist) — no duplicate checking
- Invite tracking (user clicked but didn’t complete) — binary: accepted or expired
- Multi-language invite email — English only
Data model
InviteToken: id, store_id, user_id (nullable initially), email, token_hash (SHA256), is_used, role_id, created_by, created_at, expires_at, accepted_at. Migration: add invite_tokens table with index on token_hash and store_id/email.
API
- POST /admin/team/invite
- GET /auth/invite/{token}
- POST /auth/invite/{token}/accept
- POST /admin/team/invite/{token}/resend
- POST /admin/team/invite/{token}/cancel
- GET /admin/team/invites
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Invite form: email input, role selector, ‘Send invite’ or ‘Copy link’ buttons. Pending invites table: email, role, sent date, expiration, resend/cancel buttons.
The seam — why this is core
Core owns: InviteToken table, token generation/hashing, link validity check, password-set-on-accept. Paid module owns: email delivery (or copy-paste fallback).
Core owns the interface + honest invite generator; user onboarding is infrastructure, not a support commitment or credential.
Dependencies
- admin-user-management
- password-policy-enforcement
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Creating invite generates 32-byte token, hashes it, stores hash, returns plaintext once
- Email sent with link ‘https://…/invite/abc123’; clicking shows role+permissions, prompts for password
- Submitting policy-invalid password returns rejected, form stays
- Submitting valid password creates account, is_active=true, StoreUser created, is_used=true
- Clicking same link again returns 400 Bad Request ‘Invite already used’
- Expired link (after 7 days) returns 410 Gone
- Resend generates new token, immediately invalidates old
Risks
Token guessed (weak randomness). Email not sent. Double-accept (two simultaneous requests). Invite link in forwarded emails leaks token. Old token still valid after resend.
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included; Magento: included |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.