Skip to content
Customer portal — magic link

Text or email a link and they land in their portal, no password

The fastest path to put a customer in their own self-serve portal: text or email them a magic link from the customer detail page. They tap once, they're in.

No credit card
Cancel anytime
/app/customers/<id>
'Portal link' dropdown on the customer detail page — Copy, text, or email a fresh 30-day link.

The quick version

Customer portal: Customers open their portal from a link, no password.

  • 'Portal link' dropdown on the customer detail page — Copy link / Text to customer / Email to customer
  • 30-day token expiry — sending again mints a new token, it doesn't revoke the old one
  • SMS or email delivery (operator picks per send)

No password setup, no app install, no forgotten-credentials tickets to your front desk. Every send mints a fresh 30-day token — the old one keeps working too, it isn't revoked. The mobile app's SMS-code login mints a similarly-shaped token into the same table, though the two flows currently generate different-length hex strings under the hood.

A closer look

01

Send from any customer record

From the customer detail page, click 'Portal link' → Copy link, Text to customer, or Email to customer. Sending mints a fresh token and delivers it through the existing Twilio + SMTP email stack; the button label flashes 'Texted!' or 'Emailed!' for a couple seconds as confirmation. There's no per-row send button on the customer list — this lives on each customer's own detail page.

  • 'Portal link' dropdown on the customer detail page
  • Operator picks Copy / SMS / Email per send
  • Transient button-label confirmation, no persistent 'last sent' or open-tracking
  • Sending again mints a new token — it doesn't invalidate the old one
/app/customers/<id>
From the customer detail page, click 'Portal link' → Copy link, Text to customer, or Email to customer. Sending mints a fresh token and delivers it through the existing Twilio + SMTP email stack; the button label flashes 'Texted!' or 'Emailed!' for a couple seconds as confirmation.

02

What the customer actually lands on

Tapping the link drops them at /portal/<token> — no login form, no account creation, no app install. They see their own appointments (with a link to before/after photos once a job is complete, or reschedule/cancel while upcoming), quotes, invoices with balance owed, warranty status, their vehicles, and a loyalty card. Token-scoped, so they only see their own data.

  • Appointments, with photos linked from completed jobs
  • Quotes and invoices with balance owed
  • Warranty status, cert number, and registration date (no PDF download)
  • Vehicles on file + a loyalty card
9:41
Tapping the link drops them at /portal/<token> — no login form, no account creation, no app install. They see their own appointments (with a link to before/after photos once a job is complete, or reschedule/cancel while upcoming), quotes, invoices with balance owed, warranty status, their vehicles, and a loyalty card.

03

How the token actually works

The token is stored in plain text in customer_portal_tokens (there's no hashing step) and validated server-side on every request, expiring in 30 days. Web-issued links use a 48-character hex token; the mobile app's SMS-code login flow mints a 64-character hex token into the same table. Token-scoped means no auth bleed between customers, even on a shared phone.

  • Web links: 48-char hex token
  • Mobile SMS-code login: 64-char hex token, same table
  • Server-side validation on every request
  • Token-scoped — no cross-customer leakage
  • Customer can bookmark the URL until expiry
customer_portal_tokens

🔗 Web · magic link

48-char hex token

a3f9e2c8b1...(48 chars)

randomBytes(24).toString("hex") — /portal/<token>

📱 Mobile · SMS code

64-char hex token

8h2f4d91...(64 chars)

randomBytes(32).toString("hex") — verifyCode() mints it

↓ both insert into ↓

customer_portal_tokens

id

uuid

shop_id

uuid · not null

customer_id

uuid

token

plain text

expires_at

+30 days

used_at

null until first use

One table, stored as plain text (not hashed) · 48-char (web) or 64-char (mobile) hex · 30-day expiry · reusable until it expires

Every lookup re-scopes to the token's own shop_id — a token can never resolve against another shop's data.

Everything it does

The complete list. The plan tag above shows which plans include it.

  • 'Portal link' dropdown on the customer detail page — Copy link / Text to customer / Email to customer
  • 30-day token expiry — sending again mints a new token, it doesn't revoke the old one
  • SMS or email delivery (operator picks per send)
  • Lands at /portal/<token> — no password, no signup
  • Customer sees: appointments, invoices, quotes, warranties, vehicles, loyalty card
  • Token-scoped — no auth bleed between customers
  • Works for one-off jobs (no full account needed)

A tour of the screens

Every meaningful view, the path through it, and the moments that matter — so you know exactly what you're buying.

Customer-facing portal

Lands at /portal/<token>.

Sees appointments, quotes, invoices, warranty status, vehicles, loyalty card. Token-scoped, no auth bleed.

9:41
Lands at /portal/<token>. Sees appointments, quotes, invoices, warranty status, vehicles, loyalty card. Token-scoped, no auth bleed.

Setting it up

  1. 1

    1. Operator sends the link

    From any customer page, click 'Portal link' → pick Copy, Text, or Email. A fresh token is generated and sent via the existing Twilio / SMTP email stack.

  2. 2

    2. Customer taps the link

    Lands at /portal/<token>. The token is validated server-side (expiry check only) before the page renders. Customer sees their own data scoped by that token.

  3. 3

    3. Token survives 30 days

    Expires at +30d. The operator can send again any time — that mints a new token but doesn't revoke the old one, so an older link a customer already has keeps working too. Customers can bookmark the URL and use it until it expires.

What this looks like in a shop

Illustrative scenario: Sample Shop LLC just finished a $1,840 ceramic coating. Mike opens Sarah's customer page, clicks 'Portal link' → Text to customer. She gets an SMS with a link to view her job. She taps it, sees her warranty status and her invoice. No account creation, no support call, and Mike never had to email her a PDF.

Good to know

Which plan includes customer portal?
Starter, Growth and Pro include it. See the Pricing page for everything in each plan.
What if the customer loses the link?
The operator clicks 'Portal link' → Text to customer (or Email) again from the customer page. That mints a fresh token and sends a new link — the old link, if the customer still has it, keeps working until its own 30-day expiry.
Can two customers share a portal?
Each portal is token-scoped to ONE customer record. There's no multi-customer portal for a fleet manager today — the customer mobile login also opens just the one record matching that phone number.
What about security?
The token is validated against the customer_portal_tokens table on every request and expires in 30 days. It's stored as plain text, not hashed, and it isn't single-use — anyone who has the link can use it until it expires. Fine for the customer-self-serve use case; not a substitute for staff Better-Auth sessions.

Try customer portal — no credit card.

Pick Basic, Starter, Growth or Pro on the pricing page and it's free for 30 days, no credit card. Some features need a higher plan; the pricing page shows which. Import your old data and be running by tomorrow.