Help & FAQ

How to configure and run your business on GroundCut.

← Back to dashboard

Getting started

Registering, the onboarding wizard, and what gets created for you.

What does signing up actually create?
Registering at /register creates your Supabase login, a Stripe customer, the tenant row (initially in `incomplete` plan_status), four default service zones (0–10 mi accept, 10–20 mi accept-with-fee, 20–30 mi high-value-only, 30+ mi reject), and four equipment types — then redirects you to Stripe Checkout to enter a card. Once you complete checkout, the webhook flips plan_status to `trialing` and sets your 14-day trial end. You won't be charged until day 15, and you can cancel any time from /admin/billing. The subdomain you pick at registration is permanent — choose carefully.
Step-by-step: what does the onboarding wizard cover?
/admin/onboarding has seven steps and your progress is saved in browser localStorage. (1) Welcome — orientation. (2) Your Page — set the booking page headline, description, phone, service-area blurb, and brand color. (3) Services — toggle which default services are active, optionally add a custom one. (4) Operations — set your home base address (geocoded for distance math), max service distance, max workday hours, weekend availability, default labor rate. (5) Payouts — start Stripe Connect onboarding. (6) Import — upload existing customers via CSV (optional). (7) Done. Skip any step and come back later.
Do I have to finish onboarding before customers can book?
No, but two things must be true for the booking page to actually produce quotes: home base address must be set (so distance math works) and at least one active service must exist. Both are seeded by registration but you should review them before sharing the booking link. Stripe Connect can come later — until you connect it, booking payments still go through but the funds sit on the platform side, not yours.
What does the trial give me?
After completing Stripe Checkout, your tenant lands in plan_status = "trialing" with full feature access regardless of plan choice. Plan-gated features (custom domain, route optimization, unlimited team, etc.) all work during the trial. The trial is 14 days with a card on file — you won't be charged until day 15. Cancel from /admin/billing before then to avoid the charge. If you don't cancel, the subscription auto-converts to the plan you chose at signup; the proxy will redirect /admin and /operator to /admin/billing if your subscription ever lapses.
Where do my customers reach my booking page?
By default, https://{your-subdomain}.groundcut.com — the subdomain you chose at registration. Every page on that subdomain (/, /track/{jobId}, /admin, /operator) is scoped to your tenant. Once you verify a custom domain, both URLs work and route to the same place.

Team & roles

Inviting people, what each role can do.

What roles exist and what can each do?
Four roles. Owner — the account holder; exactly one, cannot be edited or deactivated; the only role that can verify a custom domain or initiate Stripe Connect. Admin — full /admin panel including settings and billing-adjacent operations like refunds, mark-paid, and team management. Office — admin-lite; sees jobs/customers/recurring/invoices but not Settings, Billing, Team, or anything destructive. Crew — field role; only /operator and only the jobs assigned to them.
Step-by-step: how do I invite a teammate?
(1) Go to /admin/team. (2) Click "+ Invite member". (3) Enter their email (required), full name (optional), role (owner/admin/office/crew), and an optional internal hourly rate (used in bookkeeping reports, never shown to customers). (4) Save. They receive a Supabase invite email; the row shows "pending invite" until they click the link and set a password. New crew users land on /operator after their first sign-in; admin/office users land on /admin.
How do I edit or remove someone?
On /admin/team, the Edit button on any non-owner row lets you change name, role, and hourly rate. Deactivate toggles their access — deactivated users can no longer sign in but their assignment history stays intact, so reports and audit logs remain coherent. Reactivate by toggling the same control. The owner row is read-only by design — to transfer ownership, contact platform support.
What does a crew member actually see?
A crew user only sees /operator. The queue page shows three sections for the selected date: jobs assigned to them (the work they own), unassigned jobs (work they can pick up), and other crew's jobs (read-only context so they understand what else the team is doing). They can complete and modify only their own assigned jobs. They cannot reach /admin at all.
Why is the hourly rate field there if customers don't see it?
It feeds the bookkeeping panel. When you run cost reports, the labor-hours-per-job × that crew member's hourly rate is what gets booked as labor cost. Setting accurate rates means your profitability numbers reflect reality. Leave it blank and reports fall back to the tenant default labor rate from /admin/settings.

Tenant settings — full reference

Every field on /admin/settings, what it does, where it shows up, the default. Owner-only access.

Business name
Read-only — set at registration. Appears in the header on /admin and in customer notifications as the "from" name. To change it, contact platform support.
Subdomain
Read-only — set at registration. Your booking page address is {subdomain}.groundcut.com. Permanent. If you need a different subdomain you'll have to migrate the tenant; if you want a different URL just set up a custom domain instead.
Brand color (hex)
Defaults to #15803d (GroundCut green). Drives the accent color on your booking page — buttons, focus rings, the "Book now" CTA. Use the color picker or paste a hex value. Pick something readable against white; the page does not auto-pick contrasting text.
Booking page: Headline
The hero text at the top of your booking page. Default placeholder is "Professional lawn care you can count on" — replace with whatever brand voice fits. Plain text only, no markdown.
Booking page: Description
Multi-line subtext under the headline. Use it for a short pitch — what you do, where, why customers pick you. Plain text; line breaks are preserved.
Booking page: Phone (optional)
If set, displays as a tel: call link on the booking page. Customers who hesitate to fill out the form often call instead — leaving this blank means you lose those leads. Format is whatever your customers expect (e.g. "(555) 555-5555").
Booking page: Service area blurb (optional)
A small line under the address picker, like "Serving Senoia and surrounding areas". Sets customer expectations before they enter their address — a customer outside your service zones will still get the rejection message, but this blurb prevents some wasted effort.
Allow same-day bookings
Off by default. When on, the booking page offers today as a slot, provided drive time + service time still fits before sunset. When off, the earliest offered slot is tomorrow. Useful when you have surprise openings and want to fill them — keeps a steady cadence for crew if you leave it off.
Accept bookings on Saturdays / Sundays
Two independent toggles, both off by default. Off-toggled days are simply hidden from the date picker. Turn on whichever weekend days your crew works.
Service Area: Max distance (miles)
Hard cap on straight-line distance from your home base. Customers further than this are turned away at the booking step. Combined with your zones — a 30-mile reject zone accomplishes much the same thing — but max distance is the platform-wide guardrail.
Service Area: Max drive time (min)
One-way drive time cap (Mapbox-routed). A customer can be inside your max-distance radius and still get rejected if the route is gnarly enough that drive time exceeds this. Useful for areas with rivers, mountains, or sparse road networks where as-the-crow-flies distance lies.
Service Area: Max workday (hours)
Total field hours per day. Used by the booking page to decide whether a same-day slot still fits given today's already-booked work. Used by route optimization to budget the day. Set to whatever a real crew day looks like — most operators use 8.
Service Area: Home base address
Required. The address from which your crew dispatches — not necessarily your office. Type the address, click "Look up", and the geocoder resolves it to lat/lng. Every quote uses this point: distance, drive time, zone match. If the geocoding result is wrong, the displayed address will look obviously off — fix it before saving.
Default labor rate ($/hr)
The fallback hourly rate when a team member doesn't have their own rate set. Used purely by the bookkeeping panel's cost calculations — does not appear on quotes or invoices. Set it close to your actual loaded labor cost (wage + payroll tax + workers' comp).
Vehicle fuel MPG
Used by bookkeeping for fuel cost: miles × 2 (round trip) ÷ MPG × current gas price. Set to your actual truck/trailer rig MPG, not the EPA highway number. Affects reported margins, not customer pricing.
Payment collection: Via Stripe
Default mode. The booking page collects a card and charges the quoted price immediately on confirm. The job, invoice, and payment records flow automatically — you don't mark anything paid by hand. Refunds and cancellations are handled per the Invoices section below.
Payment collection: Outside the app
When on, the booking page captures the booking but takes NO payment. The customer sees your free-text "payment instructions" on the confirmation screen and on every invoice. You collect cash/check/Zelle/etc. yourself, then mark each invoice paid manually from /admin/invoices. Switch back to Stripe at any time — historical jobs aren't affected.
External payment instructions (text)
Visible only when payment_collection is "Outside the app". Free-text field shown verbatim on confirmation and invoices. Typical content: "Zelle: pay@acmelawn.com / Checks payable to Acme Lawn LLC / ACH routing 123456789, account 0001234567". The booking page does no formatting — line breaks survive but no markdown.
Deposit amount ($)
A flat dollar amount intended as a non-refundable deposit on bookings. NOTE: this field is wired into settings storage but the current booking flow charges the full quoted price at booking, so the deposit field is a placeholder for upcoming functionality. Treat it as inert until further notice — set to 0 to be explicit.
Default tax rate (%)
Single percentage applied to invoice line items at creation. Set as a number (e.g. 7 for 7%, not 0.07). For multi-jurisdictional setups (state + county), leave this at 0 and stack rates in /admin/tax-rates instead — they combine the same way.
Include card processing fee in quoted price (toggle)
Off by default. When on, the booking page adds 2.9% + $0.30 to the quote so you net the full quoted amount after Stripe takes its cut. Customers see one combined price — no separate "convenience fee" line item. If your competitors don't do this, leaving it off may make your prices look better in apples-to-apples comparisons.
Notification toggles (Booking Confirm / Job Complete / Day-Before / On the Way)
Each event has independent SMS and email toggles, all on by default. Off-toggled events are skipped entirely for that channel. SMS requires Twilio env vars (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER); email requires Resend env vars (RESEND_API_KEY, RESEND_FROM_EMAIL). Missing keys silently disable that channel platform-wide regardless of your toggles.
Message templates: how do they work?
Each customizable event (booking_confirm, job_complete, day_before, on_the_way, weather_delay, time_for_next_cut) has three editable fields: SMS body, email subject, email body. Click an event to expand. Each field uses {{variable}} placeholders — the chips at the top of the editor show available variables for that event. Leave any field blank to fall back to the built-in default. Click "Reset to default" to clear all three for that event.
Available template variables (which events offer which)
Booking Confirmation: customer_name, service_name, date, time, amount, tenant_name, track_url. Job Complete: customer_name, service_name, date, amount, tenant_name. Day-Before Reminder: customer_name, service_name, date, time, tenant_name. Crew On the Way: customer_name, service_name, tenant_name. Weather Delay: customer_name, service_name, date, tenant_name. Time For Your Next Cut: customer_name, service_name, days_since, weeks_since, tenant_name, booking_url.
Re-engagement: enable toggle
Off by default. When on, the daily re-engagement cron picks customers whose last completed job falls inside your configured day window and sends them a "time for your next cut" SMS + email. Each customer is capped at 3 messages per cycle with a 14-day cooldown; the cycle resets when they book again.
Re-engagement: earliest day after last cut
Lower bound of the eligibility window. Default 10. Min 7. A customer whose last cut was 9 days ago will not get a nudge yet at this default. Push higher if you grow longer; push lower if you do tighter weekly programs.
Re-engagement: latest day after last cut
Upper bound. Default 14, max 60. After this many days post-last-cut, the customer drops out of the re-engagement queue entirely (until their next booking). Setting it too high wastes messages on customers who have already churned to a competitor.
Timezone
IANA timezone string (e.g. America/Chicago). All scheduled times shown to crew, customers, and admins are formatted in this zone, and date filters on Books / Jobs / Customers interpret YYYY-MM-DD inputs against it. Set this once at onboarding to wherever your business actually operates — getting it wrong means times in confirmation emails and the operator queue read off by the offset.
Offer in-person estimates (toggle)
On by default. When on, the service editor lets you create services with quote_type = "in_person" — customers booking those services get an "estimate request" flow (pick a time for you to visit) instead of an instant quote. The booking confirms the visit; you send a quote afterward. Turn off if you only do instant-quote work like routine mowing. Existing in-person services keep working either way.
Customer-facing policies (5 fields: Cancellation, Refund, Weather, Inclusions, Before we arrive)
Each is a free-text field up to 2000 characters. They show up in an accordion on your booking page above the submit button, and the customer chatbot uses them as authoritative answers when customers ask cancellation/refund questions. Click "Use template" to insert a sensible default written for a lawn-care business; "Replace with template" overwrites your text. Leave any field blank to hide it from the booking page entirely.
Business address / phone / email (invoice template)
Optional. When set, these appear in the header of every invoice (PDF and HTML) and on receipt-style emails. Useful for jurisdictions where invoices need a physical address, or so customers can reach you for billing questions. Independent of the booking-page phone — that's the lead-capture number; this is the back-office one.
Invoice number prefix and notes
Prefix prepends to your sequential invoice numbers (e.g. "ACME-" → ACME-0001, ACME-0002). Notes append to every new invoice as a generic footer line — payment terms, thank-you message, return policy. Both are display-only; changing them does not renumber or rewrite past invoices.
Custom domain (text + Verify DNS button)
Owner only. Enter your domain (e.g. book.acmelawn.com), Save, then click Verify. The verifier does a DNS CNAME lookup and checks that it points at cname.vercel-dns.com (or cname-china.vercel-dns.com). When verified, the badge flips from amber "Unverified" to green "Verified" and the proxy starts routing your domain to your tenant. Save creates the row; Verify only enables routing — both steps are required. Changing the domain string resets verification.

Services & pricing — adding services and what every field does

/admin/services. Up to 20 services per tenant.

Step-by-step: how do I add a new service?
(1) Go to /admin/services. (2) Click "Add service" (top-right; the button is disabled if you already have 20). (3) In the modal, fill out Basic (name, description, base price, default crew size). (4) Fill out Pricing Rules — at minimum: price per ¼ acre, min price, max price. The other fields (target hourly rate, surcharges, large-lot rules) are all optional. (5) Fill out Time Rules — at minimum: minutes per ¼ acre. Multipliers are optional. (6) Click "Add service". You're returned to the list with the new service active by default.
How do I edit or disable a service?
On /admin/services, every row has Edit and Disable/Enable buttons. Edit opens the same modal as add, prefilled. Disable hides it from the booking page (existing jobs are not affected). Disabled services are dimmed in the list — re-enable any time. Deletion is not exposed in the UI; disable is the intended way to retire a service.
Instant quote vs in-person estimate (quote_type)
Each service is one of two types. Instant — the booking page computes a price from your pricing rules and the customer pays at booking; this is the default. In-person — the customer requests an estimate by booking a visit time (visit_duration_mins, default 60). No price is shown and no card is charged at booking. After you visit, you create an invoice manually (or convert the request to a regular booking) and the customer pays through that. In-person is for work where you can't price from a polygon — landscaping, tree removal, projects. Toggle the type when you create or edit the service. The "Offer in-person estimates" master switch in /admin/settings hides the option from the editor when off.
Service field: Name
Required. Customer-facing — exactly what they see on the booking page service picker. Defaults to the seed names (Standard / Overgrown / Debris Cleanup / Leaf Removal). Keep it short and self-explanatory.
Service field: Description
Optional. Customer-facing helper text under the name on the booking page. Use it to set expectations: "Single-cut mow with bagging" or "Includes one truckload of debris haul-off". Leave blank if the name is enough.
Service field: Base price ($)
The starting amount before any per-area math. Final quote = base_price + (price_per_quarter_acre × quarter_acres) + surcharges, then clamped to [min_price, max_price]. Default 45. Set to 0 if you want the price to be entirely area-driven.
Service field: Default crew size
How many people typically work this service. Default 1. Used by route optimization and time-budget math (workday hours × crew size = total person-hours available). Does not affect pricing.
Pricing rule: Price per ¼ acre ($)
The per-area component. A 0.5-acre lawn (= 2 quarter-acres) at $12/¼ac adds $24 on top of the base price. Default 12. The most important number to get right — too low and you lose money on big lawns, too high and you price yourself out.
Pricing rule: Target hourly rate ($) — optional
Optional revenue floor expressed as $/hr. When set, the engine computes estimated_time_mins from the time rules and ensures (final_price / estimated_hours) ≥ target_hourly_rate, bumping the price if needed. Use it as a safety net — "I never want a job that pays less than $60/hr". Leave blank to disable the floor.
Pricing rule: Min price ($)
Floor on the final quote. Default 40. Even tiny postage-stamp lawns won't quote below this. Set it to your cost-to-show-up — windshield time alone justifies a non-zero floor.
Pricing rule: Max price ($)
Cap on the final quote. Default 200. Prevents runaway prices on huge lots before any surcharges. If you serve estate properties, raise this aggressively; if you only do residential, the default is fine. Final price is min(max_price, calculated_price + surcharges).
Pricing rule: Overgrown surcharge ($) — optional
Flat dollar add when the customer ticks the "lawn is overgrown" box on the booking page. Optional. Typical values: $25–$75. Pairs with the time rules' overgrown_multiplier so your estimated time also scales up.
Pricing rule: Cleanup surcharge ($) — optional
Flat dollar add when the customer asks for cleanup (sticks, trash, landscape debris). Optional. Useful when cleanup is a meaningful portion of the work. If you always cleanup as part of standard mowing, leave this blank.
Pricing rule: Hauling surcharge ($) — optional
Flat dollar add when the customer asks you to haul off the debris (truckload to dump or compost site). Optional. Typically the largest single surcharge — a dump trip costs you time and tipping fees.
Pricing rule: Complexity surcharge ($) — optional
Flat dollar add for jobs the booking page has flagged as complex (steep slopes, lots of obstacles, hand-trim heavy). Optional. The complexity flag is set by the booking page heuristics; you can also add it manually on the job. Pairs with complexity_multiplier in time rules.
Pricing rule: Large lot threshold (acres) — optional
Activates the large-lot bump when the lawn polygon exceeds this size. Optional. Typical values 0.5 or 1.0 acres. Below the threshold the bump does nothing; at-or-above, the bump percentage applies.
Pricing rule: Large lot bump (%) — optional
Percentage uplift when the lawn meets the large-lot threshold. Optional. A 15% bump on a $200 quote becomes $230. Use it when big lots cost you more per square foot to service than small ones (e.g. you have to bring a riding mower).
Time rule: Min per ¼ acre
Required. Estimated minutes of crew-time per quarter-acre. Default 20. Drives the duration shown to crew on the operator queue, the route-optimization budget, and the target-hourly-rate floor. Time it yourself on a representative job — guessing high inflates prices, guessing low jams your route.
Time rule: Overgrown multiplier — optional
Multiplies the time estimate when the overgrown flag is set. Optional, typical 1.5. So a 30-minute mow becomes a 45-minute one. The multiplier compounds with your other multipliers — if both overgrown and large-lot apply, you multiply by both.
Time rule: Large lot multiplier — optional
Multiplies the time estimate when large-lot threshold is met. Optional, typical 1.2. A bigger lot is not just more area — turning radius, fuel-up time, and trim work scale with size in ways pure area math doesn't capture.
Time rule: Complexity multiplier — optional
Multiplies the time estimate when the complexity flag is set. Optional, typical 1.3. Used together with the complexity surcharge to keep both price and time honest about jobs that are slower than they look.
Worked example: how a quote is calculated end-to-end
Customer draws a 0.6-acre polygon, ticks "overgrown", picks Standard service. Standard has base $45, $12/¼ac, min $40, max $200, overgrown surcharge $30, large-lot threshold 0.5ac, bump 10%. Math: 0.6ac = 2.4 quarter-acres; per-area = 2.4 × $12 = $28.80; subtotal = $45 + $28.80 = $73.80; + overgrown $30 = $103.80; large-lot threshold met → ×1.10 = $114.18; clamped to [$40, $200] → $114.18. Then any service zone fee gets added. Then any active seasonal rule. Then tax. The customer sees one final number; the line items are visible on the quote summary.

Service zones

/admin/zones. How distance from your home base shapes what you accept.

What is a zone?
A distance band centered on your home base — for example, "0 to 10 miles". Each zone has a rule that decides what happens when a customer's address falls inside it: accept the booking, accept with an extra fee, accept only if the job clears a value floor, or reject outright.
What does each zone rule do?
Accept — take the booking with no extra charge. Accept + fee — tack a flat dollar fee onto the quote (covers extra drive). High-value only — apply a fee AND require the quoted price to clear a minimum, so you don't drive 30 miles for a $50 job. Reject — turn the booking away with a "we don't serve that area" message.
How do zones get evaluated?
Zones are walked in sort_order. The first zone whose distance range contains the customer's straight-line distance from your home base wins — its rule applies. If no zone matches at all (rare, since the seed includes a 30+ reject), the booking is rejected as out-of-area. Adjust sort_order to control which of overlapping zones takes precedence.
Step-by-step: adjust your zone setup
(1) Go to /admin/zones. (2) Click an existing zone to edit, or "+ Add zone" to create a new one. (3) Set name, distance range (start/end miles), rule, fee dollars (if accept-with-fee or high-value), min job value (if high-value), and sort order. (4) Save. Test by going through your booking page from a few addresses to confirm behavior.

Tax rates

/admin/tax-rates. Stack state + county + city as separate rows.

How do rates stack?
Each rate has a name (e.g. "GA State 4%"), a percentage stored as a decimal (0.04 = 4%), and an applies-to scope. The combined rate shown on the panel is the sum of active "all customers" rates. At invoice creation, the combined percentage multiplies the line-item subtotal to produce tax. Inactive rates are excluded.
Step-by-step: add a new tax rate
(1) Go to /admin/tax-rates. (2) Click "+ Add rate". (3) Enter the name, percentage (as a percent — the form converts to decimal on save), and choose scope (all customers / service-specific / customer-segment-specific). (4) Save. New rates apply to invoices going forward only — past invoices keep whatever tax was applied at the time they were created.
Tax-exempt customers
On any customer record (/admin/customers), the tax_exempt flag suppresses tax on every invoice for that customer regardless of the global rate. Use it for nonprofits, churches, government accounts, or any customer with a documented exemption. Keep their certificate on file outside the app — the audit log records the flag change but doesn't store the cert.

Seasonal pricing

/admin/seasonal-pricing. Date-bound adjustments to quotes.

What is a seasonal rule?
A date range (start + end), an adjustment type (multiplier or flat dollars), an adjustment value, an optional service scope (apply only to specific services or all), and an active toggle. While today is inside the range and the rule is active, the adjustment is applied to matching quotes as the last math step before tax.
Multiplier vs flat — what's the difference in storage?
Multiplier: stored as a number like 1.15 for +15% (or 0.90 for -10%). Applied as quote × multiplier. Flat: stored as a positive or negative dollar amount. Applied as quote + value (so use a negative number for a discount). Pick whichever model matches how you think about the change.
How do I set up "spring rush" pricing?
(1) /admin/seasonal-pricing → "+ Add rule". (2) Name it (e.g. "Spring rush"). (3) Date range: April 1 to May 31. (4) Type: multiplier. (5) Value: 1.15 (= +15%). (6) Service scope: all (or pick specific services if only some get bumped). (7) Active: on. Save. The Active Now / Upcoming / All Rules tabs let you see at a glance what's currently affecting quotes.

Recurring schedules

/admin/recurring. Putting customers on a regular cadence — billed via a Stripe subscription.

Step-by-step: set up a recurring customer
(1) Go to /admin/recurring. (2) Click "+ New schedule". (3) Pick the customer (must already exist — create them first under /admin/customers if needed). (4) Pick the service. (5) Pick the cadence (weekly, biweekly, or monthly). (6) Pick the first job date. (7) Save. If the customer has a card on file, a Stripe subscription is created at the same time and the schedule shows a "subscription" badge. If they don't, the schedule still works but billing is handled outside Stripe (mark the invoices paid manually as you collect).
How are recurring invoices paid now?
For Stripe-backed schedules, Stripe is the engine: it issues an invoice for each cycle and charges the saved card automatically. The platform mirrors each Stripe invoice into the GroundCut invoices table (source = "recurring") so it shows up on /admin/invoices and in the customer's history with a payment row attached. There is no per-cycle action you take — paid, failed, and refunded events all flow back via the customer.invoice and charge.refunded webhooks.
How are recurring jobs actually generated?
Click "Generate jobs (14 days)" on /admin/recurring whenever you want to materialize the next batch. The endpoint walks every active schedule and creates jobs for any cadence date inside the next 14 days that doesn't already have one. It returns counts of generated and skipped. Run it weekly so your operator queue is always populated. Job generation is independent of billing — Stripe handles invoices on its own cadence.
How do I pause or stop a recurring customer?
For Stripe-backed schedules, "Pause" is replaced by "Cancel subscription" — pausing would leave the live subscription billing the customer with no jobs running. Click Cancel subscription, then choose "Cancel now" (Stripe stops billing, the schedule deactivates, the customer is refunded for the unused portion of the current period) or "Cancel at period end" (schedule keeps running until the cycle ends, then deactivates). Schedules without a Stripe subscription still have the simpler Pause/Resume toggle. Deleting a schedule that's subscription-backed is blocked until you cancel first.
Can the customer cancel their own recurring service?
Yes. Recurring confirmation and renewal emails include a signed cancel link to /track/recurring/cancel. The link is valid for 90 days and tenant-scoped — no login required. The customer picks "cancel now" (immediate stop + prorated refund) or "cancel at period end". You see the result on /admin/recurring as the schedule deactivates with deactivated_reason = "canceled".
A schedule shows "unpaid" — what happened?
Stripe tried to charge the customer's card for a recurring cycle and failed (expired card, insufficient funds, etc.). After Stripe's built-in retry policy gives up, the customer.subscription.deleted webhook deactivates the schedule with deactivated_reason = "unpaid", and a follow-up appears on the dashboard. Reach out to the customer; if they update their card and want to continue, create a new schedule.
Why is my next_date in the past?
The schedule was active but no one ran the generator, so the cadence walked past today without creating jobs. The panel marks overdue schedules with a warning. Run "Generate jobs (14 days)" — the generator catches up and creates the missed dates inside the window. Anything older than the 14-day window is gone; if you want to backfill, create those jobs manually.

Customer management

/admin/customers. Adding, editing, and the per-customer flags that change behavior.

What fields does a customer have?
full_name (required), address (required), email (required — used for email notifications, Stripe receipt, and cancellation/reschedule auth on the track page), phone (required — used for SMS), price_override (optional fixed dollar override on quotes — bypasses normal pricing math), and tax_exempt (boolean). The address is what customers receive jobs at; their booking history is keyed on the customer record.
Step-by-step: add a customer manually
(1) Go to /admin/customers. (2) Click "Add customer". (3) Fill out fields. (4) Save. Useful when you sign up someone over the phone and want them on a recurring schedule without making them go through the booking page. Otherwise the booking page creates the customer automatically the first time someone books.
What does price_override do?
When set, all future quotes for this customer ignore the service's pricing math and just use this dollar value. Use it for friends-and-family pricing, contracted accounts, or customers you've negotiated a flat rate with. Leave it blank for normal pricing. It does not retroactively change past invoices.
Bulk import: how does CSV work?
On /admin/customers, click "Import CSV" and upload a customers CSV. Columns are auto-mapped (case-insensitive, underscores normalized). At minimum the file needs full_name + address; email, phone, price_override, and tax_exempt are optional. Preview shows the first 5 rows; click Import. Duplicates are skipped and reported. Available to owner and admin roles. Jobs are not bulk-importable — create them manually after the customers exist, since the columns a job needs (service, price, scheduled time) usually aren't present in tenants' historical exports.
How do I export my customer list?
On /admin/customers, click "Export CSV". The file includes name, contact info, address, tags (semicolon-separated), tax_exempt, price_override, archived state, and the SMS/email opt-out flags. Useful for migrations, mail merges, or annual data reviews. Archived customers can be excluded via the URL param (?archived=false) or included by default — the panel's filter mirrors this.
What does the customer profile page show?
Click any customer row to open their profile. The page has four tabs: Activity (job/payment/refund/tag history with timestamps), Notes (free-text notes you and other staff add — author and timestamp recorded), Reminders (one-off "follow up about X by date" items — surface in your dashboard's Follow-ups widget), and Communication (every SMS and email the platform has sent for this customer, with delivery status from Twilio/Resend). Above the tabs you see contact info, account status, tags, and Edit / Archive controls.
Tags: what are they and how do I use them?
Custom labels you define at /admin/tags (name + color). Apply them to customers from the customer profile page. Tags show as colored chips on the customers list and feed into export. Use them for any segmentation that matters to you: "VIP", "Estate property", "Snowbird", "Difficult access". Tags are tenant-scoped; deleting a tag removes it from every customer it was applied to.
Archiving vs deleting a customer
There is no destructive delete in the UI — to keep history coherent (jobs, invoices, payments, audit trail), the platform uses Archive instead. Click Archive on the customer profile, optionally enter a reason, confirm. The customer is hidden from default lists and the booking-page lookup, their recurring schedules are deactivated, and they stop receiving any notifications. Click Restore to undo. If you genuinely need to purge a record (GDPR request, data dispute), contact platform support.
Reminders — what shows up on the dashboard?
Reminders you add to a customer profile (with an optional due date) appear in the dashboard's Follow-ups widget on the due date as type = "manual". Use them for "call back about quote", "check on storm damage", anything you don't want to forget. Resolving from the dashboard or marking the reminder done on the customer page hides it.

Customer booking flow

What your customers experience on your booking page.

What steps does the customer go through?
Address → map (the customer draws their lawn polygon on a Mapbox satellite view) → service selection → time (instant-quote services) OR estimate-visit time (in-person services) → consent + policies review → payment (or "external collection" instructions) → confirmation. Each step validates before letting them advance. The polygon directly drives the quote, so a careful drawing produces a fair price. In-person services skip the price/charge step — the customer just books a visit and you send a quote afterward.
What does the customer agree to before submitting?
A required consent checkbox above the submit button covers: agreement to your /terms and /privacy pages, and explicit SMS consent ("by submitting, I agree to receive transactional and reminder texts; reply STOP to opt out") to satisfy TCPA. Above the checkbox, a collapsible Policies accordion shows whatever cancellation/refund/weather/inclusions/preparation text you set in /admin/settings — customers can read each section before agreeing. The submit button stays disabled until consent is checked.
Can customers ask questions while booking?
Yes — every booking page has a chat button in the bottom-right corner. The chatbot has read-only access to your business name, services, pricing model, and the five customer-facing policy fields you wrote in Settings. It answers questions like "do you do leaf removal?", "what's your cancellation policy?", or "how is the price calculated?". It never sees customer PII or invoices and won't help anyone bypass the booking flow. Conversation history is local to the customer's session and discarded when they close the tab.
What information does the customer have to provide?
Full name, email address, and phone number — all three are required. Email is used for booking confirmation, the tracking link, day-before reminder, and Stripe's own receipt email. Phone is used for SMS notifications (on-the-way, reminders). Both are needed for the customer to get complete service communication.
When does the customer's card get charged?
Immediately, at the moment they confirm payment on the booking page. The PaymentIntent uses Stripe's default automatic capture, so funds are captured on confirmation — there is no separate "capture" step at job completion.
What if their card is declined?
Stripe rejects the confirmation client-side and shows an error in the Payment Element. The customer's job record exists by that point (created before payment) but the invoice is set to "overdue" when the failed-payment webhook arrives. They can retry from the same screen, or you can take over from /admin/invoices.
What does the customer see if I'm on external payment collection?
No card form on the booking page. After confirming, they see a confirmation screen with your custom payment instructions (the text you set in /admin/settings) — Zelle, ACH details, check-payable name, whatever you put. They get the same instructions on every invoice email until you mark the invoice paid.

Stripe Connect — how you get paid

Connecting your bank so booking payments reach you.

What is Stripe Connect?
When a customer pays at booking, the money goes to a Stripe account. Connect lets each tenant route payments to their own Stripe-managed Express account so funds — and the responsibility for them — sit with you, not the platform. Until you finish Connect onboarding, charges still work, but the platform is the merchant of record and you receive nothing.
Step-by-step: connect your bank
(1) Go to /admin/billing. (2) Click "Connect Stripe". The app creates an Express account for your tenant and redirects to a Stripe-hosted onboarding URL. (3) Provide tax info, business details, bank account, ID verification — exactly what Stripe asks for, all inside their flow. (4) Finish; Stripe redirects you back. (5) Stripe sends a webhook; the app flips your tenant to onboarded once both charges_enabled and payouts_enabled are true. From that point, every booking PaymentIntent uses on_behalf_of with your account.
What does on_behalf_of do for me?
It makes your connected account the merchant of record for the charge. Stripe's 2.9% + $0.30 processing fee comes out of YOUR balance, not the platform's. Without on_behalf_of, the platform pays Stripe and would have to claw the fee back somehow. It also means refunds, chargebacks, and 1099-K reporting attach to your Stripe account, where they belong.
How and when do I actually get paid out?
Stripe pays out from your connected account on its standard schedule — daily by default in most regions, with a 2–7 day initial rolling delay for new accounts. The platform doesn't control this; manage your payout schedule in your Stripe dashboard.
What is the platform fee?
GroundCut's revenue comes from your monthly subscription, so the per-charge platform fee defaults to 0%. The platform owner can set a per-tenant override (capped at 10%) via the tenants.platform_fee_pct column for special arrangements; if it's set, that percentage is taken via Stripe's application_fee_amount on every booking charge and you see your net (charge − Stripe fee − application fee) on your Stripe dashboard. Existing PaymentIntents are unaffected by changes.
I haven't finished Connect — should I keep taking bookings?
Bookings still go through, but the money is sitting on the platform side and is not yours yet. Finish onboarding before you push out the booking link. If you need to take payment now and figure out Connect later, switch payment_collection to "external" in Settings — that disables card-on-book and you collect outside of Stripe.
How do I check whether Connect is fully set up?
/admin/billing shows your Connect status: a "Connected" badge if the account exists, plus an "Onboarded" indicator that flips green only when Stripe reports both charges_enabled and payouts_enabled. If you see "Connected" but not "Onboarded", you started onboarding but Stripe is waiting on documents — click "Resume onboarding" to finish.

Invoices, refunds & cancellations

What happens after the customer has paid.

When does an invoice get created?
Booking and recurring both auto-create invoices. (1) Booking — when the booking endpoint creates the job, it also creates an invoice (source = "booking", status = "sent") so the Stripe webhook for the successful charge can flip it to paid. (2) Recurring (Stripe-backed) — every cycle, Stripe issues an invoice and the customer.invoice webhook mirrors it into the GroundCut invoices table (source = "recurring") with the matching payment row. (3) Manual — you can also create one yourself from /admin/invoices/new, useful for off-platform work or one-off charges. You don't take any action for booking or recurring invoices.
What invoice statuses exist?
draft (created, not sent), sent (awaiting payment), paid (payment confirmed by webhook or manual mark-paid), partial (a partial payment is recorded — invoice stays paid but a negative payment row exists), overdue (the card-charge attempt failed), and void (fully refunded). Stripe webhooks handle status changes for booking/recurring; manual mark-paid jumps draft/sent → paid.
Step-by-step: issue a refund
(1) Go to /admin/invoices. (2) Find the paid invoice. (3) Click "Refund". (4) For a full refund, leave the amount blank and click Full. For a partial, type the dollar amount and click Partial. The app calls Stripe with reverse_transfer and refund_application_fee enabled — Stripe pulls the funds back from your Connect balance and returns the platform fee. A full refund flips the invoice to "void"; a partial leaves it "paid" but adds a negative payment row.
What happens when a customer hits Cancel on the tracking page?
They confirm with their booking email, the job moves to cancelled, the customer gets an SMS and email saying you'll be in touch about a refund, and every owner and admin on your tenant gets an email with a link to /admin/invoices. The card is NOT refunded automatically — that's your call. Cancel is only allowed before the crew starts the job (status booked or scheduled).
Customer paid in cash — how do I mark the invoice paid?
On /admin/invoices, find the row, click "Mark paid", choose a method (cash, check, ACH, Zelle, Venmo, other), and confirm. This creates a payment row of type "balance" with the method noted, flips the invoice to paid, and (if you have QuickBooks connected) syncs the invoice and payment over.
How do tips work?
After a job is complete, the crew can tap "Record tip" on the job detail page and enter an amount and optional note. This is a DB record only — no charge is made. It shows up as a tip-type payment on the invoice and feeds into your bookkeeping reports. Card-tipping after the fact isn't supported in-app yet.

Bookkeeping & QuickBooks

Reports, exports, and the Intuit integration.

What does the bookkeeping panel show?
/admin/bookkeeping is a cash-basis P&L for a date range you pick (default current month). It sources every figure from the payments table — money actually received or refunded — not from completed jobs, so what you see matches your bank deposits. Top-line cards: Income (gross), Refunds, Expenses, Net Profit, Net Income, Sales tax collected (pro-rated from each invoice's tax_amount), Tips, Mileage. The "Job costs" card overlays internal fuel/labor/equipment costs for jobs whose payment landed in the period so you get a meaningful gross-profit number.
A paid invoice doesn't appear on Books — why?
Books is sourced from the payments table, so an invoice only shows if it has at least one payment row in the date range. Two possible causes: (1) the date filter — the default is the current month, widen it if the payment landed earlier; (2) data drift — historical invoices marked paid before the payments table started getting written may not have a row. New activity (Stripe webhooks, mark-paid, recurring subscriptions, refunds) all write payment rows automatically, so this only affects legacy data.
How do I export for taxes or my accountant?
From /admin/bookkeeping you can export CSV (cash ledger with one row per payment, expenses, mileage log, summary) or IIF (the QuickBooks Desktop interchange format) for any date range. The IIF emits PAYMENT transactions for normal payments and tips, REFUND transactions for refunds, and CHECK transactions for expenses — accountants can import directly without reshaping. Both formats match exactly what's on the screen.
Step-by-step: connect QuickBooks Online
(1) Go to /admin/bookkeeping. (2) Click "Connect QuickBooks". (3) The app sends you to Intuit's OAuth screen with the accounting scope — sign in with your QuickBooks owner account. (4) Pick the company file (realm) you want to sync to. (5) Authorize. Intuit redirects back, the app exchanges the code for tokens and stores them encrypted in the quickbooks_connections table. You'll see "Connected" with the realm ID and the last-synced timestamp on the panel from then on.
What gets synced to QuickBooks, and when?
One-way sync from GroundCut to QuickBooks. When an invoice gets paid (Stripe webhook fires payment_intent.succeeded, or you click Mark Paid manually), the app upserts the customer (matched by display name to avoid duplicates), creates the QB invoice with line items, and creates the QB payment linked to that invoice. Sync is idempotent on the GroundCut invoice ID — re-running is a no-op.
A sync failed — what now?
The error doesn't block the booking flow; it's logged silently. Open /admin/bookkeeping — the connection row shows the last_error message and the last_synced_at timestamp. Common causes: revoked authorization (re-OAuth), expired refresh token (refresh tokens last ~100 days, so reconnect after long idle), or a sandbox/production mismatch. If the error mentions a missing Item or Account in your QB realm, the QB company file is missing one of the chart-of-accounts entries the sync expects — fix in QB and the next sync will succeed.
Can I disconnect or move to a different QuickBooks company?
Yes. Disconnect from /admin/bookkeeping; the encrypted tokens are removed and future invoices stop syncing. Reconnecting goes through OAuth fresh — pick a different realm at that point if needed. Past invoices are NOT retroactively re-synced — they stay in the old QB realm.
What environment variables does QuickBooks need?
Set at the platform level (you don't configure these per tenant): QUICKBOOKS_CLIENT_ID, QUICKBOOKS_CLIENT_SECRET, QUICKBOOKS_ENV (sandbox or production), and either QUICKBOOKS_REDIRECT_URI or NEXT_PUBLIC_APP_URL (the callback URL must match what's registered in your Intuit developer app). If your platform owner hasn't set these, the Connect QuickBooks button errors immediately.

Custom domain

Pointing your own domain at your booking page. Owner only.

Step-by-step: switch to your own domain
(1) /admin/settings → Custom Domain section → enter your domain (e.g. book.acmelawn.com). (2) Click Save. (3) At your DNS provider, add a CNAME record: host = book (or whatever subdomain), value = cname.vercel-dns.com. (4) Wait for DNS to propagate (usually a few minutes). (5) Back in /admin/settings, click "Verify DNS". The verifier does a CNAME lookup; when it resolves to Vercel, the badge flips from amber to green and your domain starts routing to your tenant. (6) SSL is provisioned automatically by Vercel — usually within minutes, occasionally up to 48 hours.
My DNS is set but Verify says "CNAME does not point to Vercel yet"
Three things to check. (1) DNS propagation — even a few minutes after the CNAME is added, your local resolver may have cached the old record; try a tool like dnschecker.org. (2) The exact CNAME target — must be cname.vercel-dns.com (or cname-china.vercel-dns.com). (3) Domain string match — the value in /admin/settings must exactly equal the host you configured DNS for. Saving a different domain string resets verification, so save the corrected value and verify again.
What about my groundcut.com subdomain after I switch?
It keeps working. The proxy resolves a host to a tenant in two ways — by subdomain match against the root domain, or by exact match against a verified custom_domain — and either path routes here. Both URLs point to the same booking page, the same /admin, the same /track links. Give customers either one.

Day-to-day crew workflow

How a job moves through /operator from booked to complete.

What does a crew member see on /operator?
Three sections of jobs for the selected date: jobs assigned to them (their work), unassigned jobs they could pick up, and other crew's jobs (read-only context). Each card shows customer name, address, status, service, scheduled time, estimated duration, and price. If a route has been optimized for that day, each card shows its stop number and the drive time to the next stop.
How does a job move from booked to complete?
Two taps: booked/scheduled → in_progress (tap "Start Job" — sets started_at and fires the on-the-way SMS/email to this customer) → complete (tap "Complete" — sets completed_at, records actual_time_mins, locks final_price to the quoted price, fires the completion notification, and if the route has a next stop automatically sends the on-the-way notification to the next customer). There is no charge step at completion — the customer paid at booking.
How do before/after photos work?
On the job detail page, tap "+ Before" or "+ After" — the camera or photo picker opens. Files are validated (JPEG, PNG, WEBP, HEIC, max 10 MB), uploaded to Supabase storage in a private bucket, and re-signed every time the page loads (URLs expire after 30 minutes). Photos are tied to the job forever and appear on the customer's tracking page after completion. Once the job is complete, the upload buttons disappear.
Can a crew member adjust the price?
No. Charge-on-book locks the price at the customer's confirmed quote. If the job took longer or the lawn was bigger than expected, that's a conversation the owner has with the customer outside the app — and if you decide to charge them more, do it manually from your Stripe dashboard or refund and rebook. The app does not support partial post-job upcharges.
Step-by-step: record a tip
(1) On the completed job's detail page, tap "Record tip" in the bottom bar. (2) Enter the tip amount and an optional note. (3) Save. The tip is recorded as a separate payment row of type "tip" linked to the job's invoice. It shows up in bookkeeping reports as additional income on that job. No card is charged — record-keeping only.
How does a customer reach the tracking page?
The booking confirmation includes a personal link of the form https://{tenant}.groundcut.com/track/{jobId}. The page polls every 30 seconds and shows the three-step progress (booked, in progress, complete) with timestamps, plus the lawn polygon and any uploaded photos. Customers can cancel or reschedule from this page until the crew starts the job.

Jobs (admin view) & route optimization

/admin/jobs — what admins can do that crew cannot.

What can I do from the admin Jobs table?
Filter by status and date range. Click any row to view full job detail. Reassign — change the assigned crew member. Reschedule — change scheduled_at; the customer is notified via the reschedule template. The Jobs admin view is the right place to fix mistakes (wrong crew, wrong time) without touching invoices or payments.
What is route optimization and how do I run it?
On /admin/jobs, with a single date selected, click "Plan route". The endpoint takes every booked/scheduled job on that date, computes a near-optimal stop order from your home base, and writes stop_order + drive_time_to_next_mins onto each job. Crew sees these numbers on /operator: a small green numbered circle and a "drive next: 12 min" line under each card. Re-run anytime — the latest run wins.
When does "Plan route" make sense?
Before the day starts, once you have a stable set of jobs. Re-run if you add or cancel a job during the day. The optimizer uses Mapbox routing under the hood, so accuracy depends on Mapbox's data — exotic edge cases (gated communities, ferries) may need manual override. There's no manual-override UI today; reassign jobs across crew if the auto-route looks wrong.

Customer notifications

Every message the app sends on your behalf.

What notifications go out automatically?
Eight events. Booking confirmation (right after a successful booking). Day-before reminder (cron at 12:00 UTC, jobs scheduled for tomorrow). Crew on the way (when crew taps "Start Job" — goes to this customer, and also when completing the previous job on a planned route). Job complete (when crew taps "Complete"). Job cancelled (when the customer cancels from the tracking page). Weather delay and weather reschedule (manual, when you postpone). Time for your next cut (the re-engagement cron at 14:00 UTC). Each event sends both an SMS and an email subject to your toggles.
Where do I configure templates?
/admin/settings → Message Templates. Each event expands into three editable fields: SMS body, email subject, email body. The chips at the top list the {{variables}} you can use. Leave any field blank to fall back to the built-in default. Click "Reset to default" to clear the entire override for that event.
Will I be notified of bookings or cancellations as the owner?
Cancellations: yes — every owner and admin gets an email when a customer cancels, with a link to the invoice for the refund decision. Bookings: not by default — they appear on /admin/jobs and the operator queue but do not page you. Weather alerts: yes — if you have jobs tomorrow and rain is likely, the rain-alert cron emails all owner, admin, and office users. Recurring follow-ups (unpaid card, lost subscription, new estimate request) appear in the dashboard's Follow-ups widget.
How do customers opt out of SMS or email?
SMS: customers reply STOP (or STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT) to any text. Twilio's carrier layer blocks future texts at the network level; the inbound webhook also flips sms_opt_out on every customer record matching that phone number across all tenants. START / YES / UNSTOP re-enables. Email: every marketing/re-engagement email includes a one-click unsubscribe link (RFC 8058 List-Unsubscribe) and a footer link. Clicking either flips email_opt_out on that customer. Transactional messages (booking confirmation, receipts, day-before reminders) keep going regardless — opt-out only affects marketing.
How do I see what was sent to a specific customer?
Open the customer in /admin/customers and switch to the Communication tab. It lists every notification the platform sent for that customer — SMS and email, with channel, event type, sent/delivered/failed status (Twilio status callbacks update SMS rows in near-real-time), and the timestamp. The Activity tab on the same page also shows non-notification events like job changes, payments, refunds, and tag changes.

Dashboard & follow-ups

The Follow-ups widget on /admin and what each item means.

What is the Follow-ups widget?
A panel on the /admin dashboard listing items that need your attention — things the platform notices on its own plus reminders you create yourself. Each card shows a type badge, the customer name, optional dollar amount or due date, and "Resolve" / "Dismiss" buttons. Resolve marks it handled; Dismiss hides it without resolving (useful when you've already acted outside the app). The widget hides when the list is empty.
What types of follow-ups appear automatically?
Three system types. (1) Unpaid recurring invoice — Stripe failed to charge a recurring customer's card and the retry window hasn't fully exhausted yet; reach out before they auto-cancel. (2) Recurring customer lost — Stripe gave up on the card and the subscription deactivated with reason "unpaid"; chase a new payment method or move on. (3) New estimate request — a customer booked an in-person service and is waiting for you to send a quote. Each links straight to the relevant invoice, customer, or job.
Can I add a manual follow-up?
Yes — from any customer profile page, the Reminders tab has "+ Add reminder". Enter a title, optional details, and an optional due date. On the due date it appears in the Follow-ups widget as type "manual" with a 🔔 icon. Useful for "call back about quote", "remember to bring extra blower bag", "check in after wedding". Resolving from either the widget or the customer page hides it.

Chat assistant

AI chat for your customers (booking page) and your team (/admin and /operator).

Where does the chat appear?
Three places, two modes. (1) Your booking page — customer mode; helps prospects with questions about your business and how booking works. (2) /admin pages — tenant mode; helps you and your team with how to use GroundCut. (3) /operator — tenant mode; same purpose for crew in the field. Click the bubble in the bottom-right corner of any of those pages to open it.
What does the chat know — and what does it not know?
Customer mode sees: your business name, brand color, services list with prices, your five customer-facing policy fields, and general booking-flow knowledge. It does NOT see customer PII, invoices, payments, schedules, or anything tenant-private. Tenant mode sees: how the platform works (settings, services, recurring, billing, etc.) and can answer "how do I…" questions about features. Neither mode can take actions — both are read-only assistants.
Where do conversations get stored?
In the visitor's sessionStorage (browser tab) only — discarded the moment they close the tab. Nothing about the conversation is saved on the server beyond the rate-limit counter. If you want to disable the chat for a tenant, ask platform support; there is no per-tenant toggle in /admin/settings yet.

Weather alerts & rescheduling

How the app warns you about rain.

What does the weather alert do?
A cron runs at 08:00 UTC every day. For each tenant, it pulls tomorrow's rain probability for your home base coordinates from Open-Meteo. If precipitation chance is 50% or higher AND you have at least one job scheduled tomorrow, it emails every owner, admin, and office user with the count of affected jobs and a link to the dashboard. The check is dedup'd by date so you only get one alert per day even if the cron retries.
How do I actually reschedule a rained-out day?
Open each affected job in /admin/jobs and edit scheduled_at. Saving moves the job — the customer is notified via the weather-reschedule template (SMS + email) with the old and new dates. Repeat for each affected job. There is no one-click "move every job" tool yet; if you have many jobs, working through the Jobs table in sequence is the fastest path.

Cron jobs & automation

Background tasks that run on your behalf.

What runs automatically and when?
Three cron tasks, all scheduled in vercel.json. Weather alerts at 08:00 UTC (rain warning to all owner/admin/office users). Day-before reminders at 12:00 UTC (SMS + email to customers with jobs tomorrow). Re-engagement at 14:00 UTC ("time for your next cut" to customers in your re-engagement window who haven't been nudged in 14 days). Each cron is protected by a shared secret so only the platform itself can trigger them.
Can I disable the automation if I don't want it?
Yes — toggles on /admin/settings turn off the customer-facing notifications individually. The crons still run, but they skip your tenant for any event you've disabled. Re-engagement has its own master enable/disable since some businesses don't want any outreach beyond active bookings.

Security & data

How tenants are isolated and how access is controlled.

How is my data isolated from other tenants?
Every domain-data table has a tenant_id column and a Postgres row-level security policy that restricts reads and writes to a user's own tenant. The proxy resolves the host to a tenant on every request and stamps an x-tenant-id header that downstream code uses to scope queries. A direct API call from one tenant's session can't see another tenant's data.
Who can issue refunds, change settings, or onboard Stripe?
Owner and admin only. Crew users have no access to /admin at all; office users see most of /admin but not Settings, Billing, Team, or anything that touches money or auth. The owner is the single person who can change the subdomain target, verify a custom domain, or initiate Stripe Connect.
How are admin actions audited?
Material changes — completing a job, refunding an invoice, marking paid, reassigning a job, archiving a customer, changing tags, canceling a recurring schedule — write to the audit_log table with the actor's user ID, the entity, the action, and a snapshot of what changed. The log is append-only. Owners can download a full CSV export from /admin/settings → Audit Log → "Export audit log (CSV)".
How are customer self-service links secured?
Customers don't have user accounts, so links sent in confirmation/reminder emails carry signed tokens instead. The recurring-cancel link uses a tenant-scoped HMAC token valid for 90 days; the email-unsubscribe link uses the same token scheme (RFC 8058 List-Unsubscribe-Post compatible, also valid 90 days). Tokens are bound to a customer + tenant and verified server-side before any change is made — passing one customer's token to another customer's record fails the check.
How are SMS opt-outs and email unsubscribes tracked?
Two boolean columns on customers: sms_opt_out and email_opt_out, plus opt_out_source noting how they opted out (sms_stop, email_unsubscribe, or admin override). The notify helper (lib/notify.ts) checks the relevant column before sending any SMS or marketing email — opted-out recipients are skipped silently. Transactional messages (booking confirmations, receipts, day-before reminders) bypass the marketing-only gate. SMS opt-outs from one customer phone number apply to every customer record across all tenants that share that number, since carriers enforce STOP at the network layer regardless of tenant.

Troubleshooting

Things that go wrong and where to look.

A customer says they were charged but I see no booking
Check /admin/jobs and /admin/invoices — the job and invoice are created before the payment confirmation, so they should both exist regardless. If the job is there but the invoice is "sent" instead of "paid", the payment_intent.succeeded webhook hasn't arrived; check your Stripe dashboard to confirm the charge succeeded, then check that webhook delivery is healthy in your Stripe webhook settings. If the charge succeeded but no webhook fired, the platform-level webhook signing secret may be misconfigured — contact support.
Booking page rejects every quote with "we don't serve that area"
Two likely causes. (1) Home base address is missing or wrong (check /admin/settings — the geocoded lat/lng has to be in the right place). (2) Zones are misconfigured — the 0–10 mi accept zone got deleted, or every zone is set to "reject", or the customer's real distance falls outside every range. Re-add a generous default zone and re-test from a known-good address.
My team can't sign in after I invited them
Invitation emails go through Resend and Supabase auth. If a teammate didn't get the email, check Resend's dashboard for delivery and bounces. If the link expired (Supabase invites have a TTL), resend from /admin/team. If they get to the password screen but the post-login page errors, their user row may be missing tenant_id — contact support.
QuickBooks shows "last error" and last_synced_at hasn't advanced
Open /admin/bookkeeping and read the last_error message. If it's an auth error, your refresh token has likely expired (>100 days idle) — disconnect and reconnect. If it's a 4xx, the QB realm probably doesn't have an item or account the sync expected; reconnect to the right company file or fix the chart of accounts in QB. Past invoices won't back-sync automatically; mark them paid again to retrigger the sync, or import them via the IIF export.
A customer's SMS or email never arrived
Check the relevant toggle in /admin/settings → Customer Notifications — it may be off. Check the customer record for a valid phone (E.164-able) or email. Check Twilio/Resend dashboards for delivery status. SMS to numbers shorter than 10 digits is silently dropped by the notify helper. Email bounces show in Resend; SMS undeliverables show in Twilio.
My crew can't complete a job
Three most common causes. (1) Job status is not in_progress — completion only works from in_progress. The "Start Job" button advances the status; it only appears on booked or scheduled jobs. (2) The job is not assigned to them. Reassign it to them or assign it to "anyone" so they can pick it up. (3) Their account is deactivated — confirm in /admin/team.
A recurring customer says they were charged after canceling
If they canceled "at period end", Stripe keeps the subscription active and charges through the end of the current cycle, then deactivates — that's a feature, not a bug, and matches Stripe's prorate-down semantics. If they canceled "now" but were still charged, check the recurring schedule on /admin/recurring: the deactivated_at timestamp tells you when the cancel actually landed; any charge dated after that is anomalous and worth a refund through /admin/invoices. The customer's cancel link is signed and tenant-scoped — passing someone else's link doesn't work, so a successful customer-side cancel is logged regardless of which side initiated it.

Customer self-service

What customers can do on their own without contacting you.

What can a customer do without logging in?
Customers don't have user accounts, but every email/SMS sent to them includes signed links for the actions they can take. From the booking confirmation: open the tracking page, cancel the upcoming job (until crew starts), reschedule, view photos after completion. From any marketing/re-engagement email: one-click unsubscribe. From recurring confirmation/renewal emails: cancel the subscription (now or at period end) via /track/recurring/cancel. SMS: reply STOP at any time to stop receiving texts.
How does the tracking page work?
Each booking confirmation includes a personal link of the form https://{tenant}.groundcut.com/track/{jobId}. The page polls every 30 seconds and shows a three-step progress bar (booked → in progress → complete) with timestamps, the lawn polygon, and any uploaded photos after completion. Customers can cancel or reschedule from this page until the crew taps "Start Job". Cancellations notify owner + admin via email; reschedules update the job and re-fire the appropriate template.
What if a customer's opt-out wasn't honored?
First check the customer record in /admin/customers — sms_opt_out and email_opt_out should be true if they used the standard channels (SMS STOP or email unsubscribe link). If both are false but they say they opted out, check that the opt-out source matches: it might have been logged against a different tenant's customer row that shares the same phone number (SMS opt-outs apply across tenants, email opt-outs are tenant-scoped). You can flip both flags manually from the customer profile to honor their request immediately.

Didn’t find your answer?

Check the Settings panel for configuration options.

Contact GroundCut support: groundcutadmin@aipromptenterprises.com