Client Portal

The Client Portal

A branded, self-serve site where Landscapt clients view their account, pay invoices, and act on estimates — entirely separate from staff login.

What it is

The client portal is a separate, public-facing Next.js route tree at /portal, distinct from the staff app. It is not a preview or an embed — it's its own login, its own layout shell, and its own set of pages, all scoped to a single Client record.

A client can view their balance and pay invoices online, see upcoming and completed job visits, review and act on open estimates (accept with an e-signature, decline, or request changes), submit support tickets, browse a shared document library, and manage their own contact info and saved payment method. None of this requires a staff account — portal users sign in with their own login, linked to their client record by a portal link, separate from staff access.

Every portal page is a server component that resolves the signed-in user to a client_id + org_id pair via getPortalContext(), then queries that client's own data — the same tables Landscapt staff use (crm_invoices, crm_job_visits, estimates, tickets), just filtered down to one client.

Giving a client access

Clients don't self-register. Access is invite-only, and staff control it from the client record:

  1. Open the client in Landscapt CRM, open the … menu on the client header, and choose Send Portal Invite.
  2. Confirm (or edit) the email address. The dialog reads the client's current portal state: if an invite is already out, the button says Re-send invite and the dialog notes whether the earlier one has expired or simply hasn't been accepted yet; if the client already has an active portal login, it says so (with their last login) and offers Revoke access & send new invite instead — a client can only have one active portal account at a time.
  3. An invite email goes out via Resend, branded with the org's name, containing a link to /portal/register/[token]. If email delivery fails, the invite link is still returned to staff so it can be shared manually.
  4. The invite expires after 7 days. Sending a new invite for the same client automatically revokes any still-pending one.
  5. The client opens the link, sees the email pre-filled (read-only), sets a password (minimum 8 characters, one uppercase, one lowercase, one number), and is signed in automatically.

Checking where a client stands. The client's Details tab has a Client Portal block showing the current status: No access (never invited), Invited with the invite date and expiry — or Invite expired once the 7 days are up — or Active with the login email, registration date, and last login ("Never signed in" if they registered but haven't been back). A button beneath it reads Send portal invite, Re-send invite, or Manage portal access depending on that status, and opens the same dialog as the … menu. Start here when a client says they can't log in.

Revoking access is the mirror action: the same … menu's portal dialog offers a reset that soft-deletes the client_portal_users row (recording deleted_at, per this project's no-hard-delete rule) and also deletes the underlying Supabase Auth user outright, so the email address becomes available for a fresh invite. Any still-pending invite for that client is revoked at the same time.

Signing in, and multi-company clients

Portal sign-in lives at /portal/login — a completely separate page from staff login, with its own email/password form. On sign-in the app looks for a live portal link for that login. A login with no portal link at all (a staff member or crew typing their own login here) is signed back out with "This is a staff or crew login, not a client portal account" and a link to the regular sign-in page. A login whose portal access was removed — access reset, client deleted, or the portal turned off for that company — is told "Your portal access is no longer active" instead of being sent round the login form again. An owner or staff member who is also a client of another company can sign in here with the same login. First-time visitors can also paste their invite link or bare token directly into the login page instead of clicking through email.

A client who forgets their password uses Forgot password? on the portal login page — it works while signed out and sends a reset email. On the Account page the phone number must be a real number (at least 10 digits); a blank value clears it. Text replies and STOP requests are matched by that number, so a malformed value would break them.

One real edge case the code explicitly handles: the same email address can be a client of two different Landscapt-using organizations. If someone accepts a second company's invite using an email that already has a portal account, registration links the new company's client_portal_users row to the existing auth user rather than creating a second login — the person then signs in once and picks which company to view.

That selection happens at /portal/select-org: if a signed-in portal user has more than one active org and hasn't chosen one yet (tracked via a portal_org_id cookie), they land on a picker showing each company's name, logo/accent color, and the client name under that company, before being dropped into that org's portal. A "Switch company" link on the Account page lets them return to the picker later.

Billing and online payment

The Billing tab lists every invoice split into Outstanding and History, with a running balance-due total and a PDF download for each invoice. A client with exactly one outstanding invoice gets a one-click Pay Now; with more than one, they can pay one from its row or tick several (or Select all unpaid) and pay them in a single payment. Invoices that belong to different accounts, such as a property manager's sub-accounts, are paid separately.

Below the invoices, Payments & Credits lists each payment, credit and prepayment with the invoices it was applied to. A Make a prepayment button lets the client pay ahead by card or bank transfer ($1.00 to $25,000.00). It lands as account credit that settles their next invoice, the same as a prepayment you enter yourself.

Payment runs through Stripe (via Stripe Elements/Payment Element), scoped to the org's connected Stripe account. Clients choose Card or Bank Transfer (ACH); starting a payment creates a payment intent, and the confirmation screen shows the amount due, any processing fee, and the total charge before the client confirms. On success the invoice balance updates within a few seconds (the UI refreshes the page automatically). If the org has no Stripe publishable key configured, the dialog simply says online payments aren't available yet, in place of the payment form. If your company hasn't connected Stripe (or hasn't finished its setup), the Billing tab hides the Pay and prepayment buttons and shows a note asking the client to contact you to pay, and the Home tab's balance button reads View Billing instead of Pay Now.

Make a prepayment lets a client put money on account ahead of their next invoice. A property manager with sub-accounts picks which account the prepayment is for (their own or one of the sub-accounts); the credit only applies to that account's invoices. If a bank-transfer prepayment is later returned by the bank, it is logged on the client's timeline and fires the same Credit card charge failed automation trigger as a failed invoice payment.

The public invoice link (the one in the invoice email, no login needed) also offers Bank transfer (ACH) next to Card when bank transfer is enabled for the org. There is no card fee on a bank transfer, and it takes a few business days to clear before the balance updates.

A client can also save a payment method on their Account page for reuse, and remove it later; the Billing tab links there rather than duplicating that management UI.

What stays hidden, and sharing tickets

  • Drafts are never shown. Draft invoices and unsent estimates don't appear in the portal, and a draft can't be paid from it. A fully paid or $0 invoice doesn't count as outstanding or flag the account as past due.
  • Real PDFs. The invoice and estimate PDF buttons in the portal download the same PDF staff and the emailed attachment use.
  • Sharing a ticket. Tickets submitted by the client from the portal show there automatically. For a ticket you created, turn on the Show in client portal switch on the ticket (or when creating it) — it needs a client linked to the ticket. The client sees only the subject, category, status, and description — never comments, files, or the assignee. Turn the switch off to hide it again.
  • Visit names. Visits in the schedule are named by their service rather than a generic job title.

Reviewing and accepting estimates

Only estimates the org has sent to that client (stage sent) show up, split into Pending Review and History. The portal shows the last sent version of the estimate — if staff have edited it since, Accept is refused until it's sent again. Acceptance is also refused once the estimate's Valid until date has passed (the client can still request changes). Three actions are available on an open, non-expired estimate:

  • Review & Accept — opens a dialog rendering the estimate's line items (respecting the same section grouping and display settings — quantities, line totals, section subtotals — staff configured on the estimate itself). If the estimate has line items, each one has its own checkbox, pre-checked, so a client can accept only part of an estimate — except a package, which shows as one line with one checkbox and is accepted or declined whole; the dialog recalculates the selected total live. Typing a full name into the signature field and confirming posts the acceptance (and which line items were accepted, if partial) to the estimate. Lines the client left unchecked are marked lost on the estimate and are excluded from the Accepted Estimates by Service reports.
  • Decline — a confirmation dialog that notifies the org's team; no reason is required from the client. On the staff side the estimate moves to the Lost stage with the reason "Declined by client via portal" — the same outcome the office would record by hand, so it counts against Close Ratios and drops out of the open pipeline immediately.
  • Request Changes — a free-text message sent back to the org without accepting or declining; the estimate stays open and the button is replaced with a "Request sent" confirmation.
An estimate past its expires_at date is shown as Expired and its action buttons disappear, even if its underlying status is still sent or viewed.

Branding and portal settings

Staff configure the portal from Landscapt Settings → Client Portal. Every Landscapt Settings tab can be linked to directly with ?tab= — for example /crm/settings?tab=client-portal opens this tab, and switching tabs updates the address bar so the link you copy reopens the tab you were on. Settings are per-org — there is one client_portal_settings row per org, and everything a client sees is white-labeled to that org's own name, color, and logo rather than showing "Equipt" or "Landscapt" branding anywhere in the shell.

Company NameShown in the portal header, browser tab area, and outbound emails (invite email, etc.). Falls back to the org’s own name if blank.
Accent ColorUsed for the logo-placeholder tile and various accents when no logo is set.
Logo URLA public image URL. Recommended 200×60px PNG or SVG. Replaces the accent-color letter tile in the header when set.
Welcome MessageFree text; stored but not yet rendered anywhere in the current portal pages.
Support Phone / Support EmailShown in the header (phone) and mobile menu / footer (phone and email) as tel: and mailto: links.
Show EstimatesToggles the Estimates nav tab.
Allow TicketsToggles the Tickets nav tab and the ability to submit new tickets.
Document LibraryToggles the Documents nav tab.
Visible Ticket CategoriesA checklist of the org’s CRM ticket categories (Settings → CRM). Only checked categories appear in the client-facing “New Ticket” category dropdown — keep internal-only categories like “Collections” unchecked.

The same settings page also hosts the Document Library manager: staff upload files (title, category, optional description) that appear identically to every client of that org — it's one shared library, not per-client. Clients download files via a short-lived (5-minute) signed Supabase Storage URL generated on demand, so nothing client-facing ever holds a permanent public link to the file.

Gotchas

  • No portal access configured. A client with no client_portal_users row simply has no way in — there's no self-signup. If a client says they can't log in, check the portal status on their Details tab (No access / Invited / Invite expired / Active) before assuming a bug.
  • One portal account per client, but one login can span multiple clients/orgs. Don't confuse the two directions: a given Client record can only have one active portal account, but a given email address/person can be linked to several different orgs' client records and switch between them at /portal/select-org.
  • Feature toggles block the page, not just the tab. Turning off "Show Estimates" also blocks a bookmarked /portal/estimates visit and the accept, decline and request-changes actions. If you extend these pages, check the setting on the server too rather than relying on the hidden tab.

Good to know

  • Property managers see their sub-accounts. A client with sub-accounts (commercial property managers) sees the sub-accounts' invoices, estimates, visits and tickets alongside their own when they sign in, each tagged with the sub-account's name. They can pay a sub-account's invoice (the payment is recorded on that sub-account's invoice) and choose which account a new ticket is for. Saved cards and autopay stay with the account that saved them.
  • Autopay is the client's choice. On the Account page a client with a saved card sees an Autopay switch. Saving a card has a “Use this card for autopay” checkbox that starts unticked, and removing the saved card turns autopay off.
  • Accepting an invite while signed in. If the invited person is already signed in with the invited email (for example an owner who is also a customer of another company), the invite page shows Add this company to my account with no password fields.
  • Accepted estimates stay Accepted. An estimate the office has converted to a job still shows as Accepted in the portal, never as expired.
© 2026 Landscapt. All rights reserved.