# Xylo CMS

> Concatenated owner-facing documentation. Individual pages: `/cms/docs/<slug>.md`.

<!-- source: https://www.xylosoftware.com/cms/docs/index.md -->

# What your site can do

These pages are for the person who runs the business. You do not need to be a developer. Most day-to-day work happens in admin after you sign in.

The screenshots show Brightside Studio, the example site that ships with the product. Your pages will use your name, colors, and words.

![Brightside Studio public homepage](./assets/public-home.webp)

## What you already have

Every site includes:

- A public website visitors can read on a phone or computer
- An admin area for you and your staff
- A leads board for people who fill in a form
- A customer list (the people you serve)

You can also turn on extra tools. A gym might turn on plans, session packs, and class booking. A plumber might turn on quotes and invoices. See [Turn features on and off](./turning-features-on.md).

## What you do in admin

Sign in at `/admin`. From there you can:

- See who needs a call
- Move leads through your pipeline
- Add customers and invite them to sign in
- Upload files and copy a link
- Record cash or a check
- Send invoices and quotes, if those tools are on
- Add membership plans and session packs, if those tools are on
- Run the weekly schedule, if group sessions are on

A tour of each screen is in [A tour of admin](./admin.md).

![Admin dashboard for Brightside Studio](./assets/admin-dashboard.webp)

## What you ask your agent to do

Your AI agent works in your site’s GitHub repo. Ask it in plain words. Good examples:

- “Change the homepage headline to …”
- “Add a blog post from this Instagram caption.”
- “Turn on memberships.”
- “Make plans bill every 28 days.”

The agent opens a pull request. You look at the change, then merge it. That merge is what goes live. Lots of example requests: [What to ask your agent](./ask-your-agent.md).

## What this docs set is not

The [Xylo CMS page](/cms) is the public offering. These pages are the how-to. Technical hosting, Stripe keys, backups, and updates sit in the **Technical** section. Start there only when you are going live or changing hosting.

## If you take memberships

Read [Memberships and plans](./memberships.md), then [Stripe setup](./stripe.md). Plans can bill monthly, yearly, every 28 days, or on another interval you choose. Session packs are separate: [Session packs](./session-packs.md).

## Contact

Xylo does not take phone or web-form sign-up for the CMS. Email xylo@xylosoftware.dev.

---

<!-- source: https://www.xylosoftware.com/cms/docs/getting-started.md -->

# Sign in to admin

Admin is the staff area at `/admin`. Customers use a different area, the portal. You sign in on the same page.

## Open the sign-in page

Go to `https://YOUR-DOMAIN/sign-in`.

![Sign-in page for Brightside Studio, with numbered markers on email, password, and Sign in](./assets/sign-in.webp)

Orange numbers on the screenshot:

1. Type your email
2. Type your password
3. Press **Sign in**

You can also press **Email me a sign-in code instead**, then enter the code from your inbox. If you forget your password, use **Forgot it?**

On a phone:

![Sign-in page on a phone, with the Sign in button marked](./assets/sign-in-mobile.webp)

## Who can get in

Staff are people on the staff list. The first owner is invited when the site is set up. After that, the owner invites others from admin → **Staff**.

Customers only reach `/portal` if the customer portal is on and someone invited them. See [Customer sign-in](./customer-portal.md).

After sign-in:

- Staff land in [admin](./admin.md)
- Customers land in the portal
- Someone who is both is treated as staff first

If you see “no access”, you are signed in but not on the staff list and not invited as a customer. Ask the owner to invite you.

## Change your password

In admin, open **My account**. Press **Change password**. Use at least 10 characters.

![My account in admin, with Change password marked](./assets/admin-account.webp)

## First things to check

1. Look at the left-hand menu. Items like Schedule, Memberships, and Payments only appear when that tool is on.
2. Open **Customers**. Add a person, or convert a won [lead](./leads.md).
3. Do not send real buyers to checkout until [Stripe setup](./stripe.md) is done on **your** Stripe account.

Until Stripe keys exist, local and preview deploys pretend to take payment. Nothing is charged. Production pay links say online payment is not set up. Staff can still [record cash and checks](./payments.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/admin.md -->

# A tour of admin

Admin is the signed-in staff area. It is not the customer portal. Sign in at `/sign-in`, then you land on the dashboard.

![Admin dashboard for Brightside Studio, with markers on Dashboard, Leads, and My account](./assets/admin-dashboard.webp)

Orange numbers on the screenshot:

1. The dashboard heading. Counts below tell you who needs a call.
2. **Leads** in the left-hand menu
3. **My account**, where you change your password

On a phone the menu is a row across the top:

![Admin dashboard on a phone, with Leads in the top menu marked](./assets/admin-dashboard-mobile.webp)

The left-hand menu changes with your tools. Dashboard, Leads, Customers, Staff, and My account are always there. The rest appear only when that feature is on.

## Dashboard

The home screen. It tells you who needs a call and where recent leads came from.

You will see counts such as **Waiting for a call**, **New this week**, and **New this month**. Stage tiles follow the pipeline in your site config. **Latest leads** and **Open pipeline** take you to the leads board.

If nobody has filled a form yet, it says so. That is normal on a new site.

## Leads

Everyone who reached out through the site. Board and list views, search, and a program filter. Open a lead to text them, log a call, leave a note, or move their stage. When they are **Won**, you can make them a customer. Details: [Leads](./leads.md).

## Customers

The people you serve. Search, add someone, invite them to sign in, archive, and open a record. Quotes, invoices, bookings, and plans all point at this list. Details: [Customers](./customers.md).

## Schedule

Weekly group sessions, closed days, and who signed up. Only shows when group sessions are on. Details: [Group sessions](./group-sessions.md).

## Memberships

Who is on a plan. Owners can turn **Let members cancel online** on or off here. Plans themselves live in Catalog. Details: [Memberships and plans](./memberships.md).

## Catalog

Nav label **Catalog**. Page title **Products & services**.

Owners add:

- Membership plans
- Session packs
- Quote line items, if quotes are on

Staff can look. Only an owner can change prices. Details on each kind of offer live on the money pages.

## Invoices

What a customer owes. Create, send, record a payment, void, or mark bad debt. Details: [Invoices](./invoices.md).

## Quotes

Priced offers. Send a link. The customer signs or asks for a change. Details: [Quotes](./quotes.md).

## Payments

Money that came in online, plus cash and checks you record. Owners can refund. Details: [Payments](./payments.md).

## Files

Upload a PDF, text file, or zip. Copy a stable link to put on the site. Details: [Files](./files.md).

## Staff

The team list. Staff can work leads. Owners invite people, set the role to **Staff** or **Owner**, and remove people.

![Staff page, with the team list and invite form marked](./assets/admin-staff.webp)

Orange numbers:

1. The team list
2. The invite form on the right

## My account

Change your password. At least 10 characters.

![My account, with Change password marked](./assets/admin-account.webp)

## View site

Opens the public site in a new tab.

## What only an owner can do

- Invite and remove staff, and change their role
- Add or edit catalog prices, plans, and packs
- Turn **Let members cancel online** on or off
- Refund a payment
- See any extra **Tools** links (for example a mailbox of emails the site sent)

Staff can still cancel a membership at period end, record payments, send invoices, and run the schedule.

## What you cannot change in admin today

Feature switches, the words on your public pages, follow-up texts, and most booking rules live in site config. There is no Settings page in admin yet. Ask your agent. See [Turn features on and off](./turning-features-on.md) and [What to ask your agent](./ask-your-agent.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/public-site.md -->

# Edit pages and content

The public site is what visitors already know how to use. Home, about, blog, contact, and your other pages. These pages are shared between visitors. They do not know who is signed in.

![Brightside Studio public homepage](./assets/public-home.webp)

On a phone:

![Brightside Studio homepage on a phone](./assets/public-home-mobile.webp)

## What you can change yourself

In admin you can:

- Upload files and copy a link ([Files](./files.md))
- Add customers and invite them to the portal
- Run the schedule, if that tool is on

You cannot rewrite homepage copy from admin today. Ask your agent. Typical requests:

- “Change the homepage headline to …”
- “Swap the hero photo for this image.”
- “Add a page for personal training.”
- “Put this Instagram caption on the blog.”

The agent edits your site files, opens a pull request, and you merge it. That merge is what visitors see.

## Blog posts and the link page

Every site can have a blog and a link-in-bio page at `/links`. Ask your agent to add a post. The `add-blog-post` skill is built for that: send a caption and a photo.

## Forms

Forms on the public site create [leads](./leads.md). Your agent can change the questions, the consent wording, and which program a form is for.

Do not take the consent box off a form that sends texts. The wording has to stay in line with your terms of service.

## Pages that are not “the website”

Some links are special:

- `/sign-in` — staff and customers
- `/portal` — signed-in customers, if the portal is on
- `/schedule` — the public timetable, if group sessions are on
- `/pay/{token}`, `/i/{token}`, `/q/{token}` — pay, invoice, and quote links you send to one person
- `/files/{slug}` — a file you uploaded

Those pages are part of the product. You do not hand-edit them like a blog post.

## Contact details

Phone, text-us, and address on the public site come from site config. If you have not set a phone number, the phone link simply does not show. Ask your agent to update contact details. Do not invent a number.

---

<!-- source: https://www.xylosoftware.com/cms/docs/leads.md -->

# Leads

Leads are people who reached out through the site. They are always on. There is no switch to hide the board.

## Find a lead

Admin → **Leads**. Use **Board** or **List**. Search by name, email, or phone. Filter by program (what they asked about). Press **Filter**.

![Leads board, with markers on the heading, a lead card, and search](./assets/admin-leads.webp)

Orange numbers:

1. The Leads screen
2. A lead card. Press the name to open it.
3. Search

On a phone:

![Leads board on a phone](./assets/admin-leads-mobile.webp)

## Open a lead

The header has their stage. Demo stages are **New**, **Contacted**, **Booked**, **First visit done**, **Won**, and **Lost**. Your site can use different names. Ask your agent to change the pipeline.

![A lead record, with the stage menu and reach-out box marked](./assets/admin-lead-detail.webp)

Orange numbers:

1. **Stage.** Pick a new stage to move them. That is how you “move a lead.”
2. **Reach out** — text, log a call, or leave a note

**Reach out** has three tabs:

- **Text** — send a text if they opted in
- **Log call** — note a phone call
- **Note** — save a note for staff

You can also **Call** or **Text from my phone**.

## Make them a customer

Move the lead to **Won**. Then press **Make them a customer** (your site may say “member” or “client” instead). That creates a [customer](./customers.md) record. Quotes, invoices, and plans need that record.

If the button errors, they are not in Won yet, or they already have a customer record.

## How leads arrive

A visitor fills a form on the public site. That form’s kind is `lead`. A won path later becomes a customer.

A form of kind `request` only emails staff. It does not create a lead.

Spam-trap submissions are rejected with a visible error. If a visitor sees that, ask them to contact you directly. Browser autofill can fill a hidden field by mistake.

## Follow-up messages

The site can text or email a lead on a schedule. That lives in site config, not in a settings screen. Ask your agent to change the wording or timing. See [Messages](./messages.md).

## What you cannot do here

You cannot turn the leads board off from admin. Turning leads off so forms only email you is still planned. Until then, every lead form creates a lead.

---

<!-- source: https://www.xylosoftware.com/cms/docs/customers.md -->

# Customers

Everyone you serve is one customer record. A “member” is a customer with an open plan. There is not a second list.

Your site may say **members**, **clients**, or **customers**. That word comes from [vocabulary](./vocabulary.md). The screen is the same.

## Find or add someone

Admin → **Customers**. Search by name, email, phone, or company. **Show archived customers** if you need an old record.

![Customers list with the add form marked](./assets/admin-customers.webp)

Use **Add a customer** on the right.

![Add a customer form, with markers on the form, invite checkbox, and Add and send invite](./assets/admin-customers-add.webp)

Orange numbers:

1. The add form. Fill name, email, and phone.
2. Tick **Invite them to sign in** if they should use the portal.
3. Press **Add and send invite** (or **Add** if you leave the box unticked).

## Invite someone who is already on the list

Open the person. If they cannot sign in yet, press **Invite to sign in**.

![A customer record with Invite to sign in marked](./assets/admin-customer-invite.webp)

Orange numbers:

1. **Invite to sign in**

## Open a customer

You can:

- Edit **Details** and **Save**
- **Invite to sign in** so they can use the portal
- **Archive** or **Restore**
- Add or remove **Locations**
- See upcoming sessions and attendance, if booking is on
- See their plan, if memberships are on
- See session credits, if packs are on
- Open **New invoice**, if invoices are on

## Session credits on the customer page

If session packs are on, staff can **Update credits** and **Check in / use credit**. That uses one credit without going through the booking screen. Booking a session also uses a credit when packs are on. Details: [Session packs](./session-packs.md).

## Won leads

A lead in **Won** can become a customer. Open the lead and press **Make them a customer**. See [Leads](./leads.md).

## What customers see

If the [customer portal](./customer-portal.md) is on, invited people sign in at `/portal`. They see their own plan, packs, sessions, quotes, and invoices. They never see admin.

---

<!-- source: https://www.xylosoftware.com/cms/docs/customer-portal.md -->

# Customer sign-in

The portal is `/portal`. It is for customers, not staff. It only exists when the `customerPortal` switch is on.

![Customer portal home for Brightside Studio](./assets/portal-home.webp)

On a phone:

![Customer portal home on a phone](./assets/portal-home-mobile.webp)

## Invite someone

Admin → **Customers** → open the person → **Invite to sign in**. They get an email, set a password, and land in the portal.

![A customer record with Invite to sign in marked](./assets/admin-customer-invite.webp)

Orange numbers:

1. **Invite to sign in**

They can also sign in with a one-time code emailed to that address. Ask them to use `/sign-in`. Staff and customers share that page. After sign-in, customers go to `/portal`.

There is no public sign-up form. If someone is not a customer yet, add them first.

## What they see at home

The home page stacks a card from each tool that has something for them:

- Their plan, if memberships are on
- Their packs, if session packs are on
- Their sessions, if group sessions are on
- Open quotes
- Open invoices

They can open **Account** to change their password. On a phone, they may see **Install portal** or **How to install**. That adds a home-screen icon. It does not store private data on the phone.

## Booking, plans, and packs

- Book a session at `/portal/sessions` when group session booking is on
- Join or manage a plan at `/portal/memberships`
- Buy a pack at `/portal/session-packs`

![Customer portal plan page](./assets/portal-memberships.webp)

Link those pages from your public site if you want a “join” or “book” button. The person still has to be signed in.

## New request

If you turned on a request form for the portal, they see a **New request** (or the label you chose). On the demo that form still creates a [lead](./leads.md).

## What they cannot do

They cannot see other customers. They cannot open admin. They cannot cancel a plan online if you turned that off in [Memberships](./memberships.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/files.md -->

# Files

Staff upload PDFs, text files, and zips in admin → **Files**. Each file gets a stable link you can paste on the site, in a text, or in the portal.

## Upload a file

Press **Upload a file**, pick the file, then **Upload**.

![Files page with the upload form and Upload marked](./assets/admin-files.webp)

Orange numbers:

1. **Upload a file**
2. **Upload**

If nobody has uploaded yet, the page says **No files yet. Upload one to get a link for it.**

## Copy the link

Each file has **Copy link**. Paste that on a page, in a text, or in the portal.

![A file row with Copy link marked](./assets/admin-files-copy.webp)

Orange numbers:

1. **Copy link**

You can **Replace** or **Delete** later.

## Who can open the link

When the customer portal is on, you pick:

- **Anyone**
- **Signed-in customers only** (your site may say members or clients)

Signed-in staff can open either kind. Put a signed-in-only link on a portal page, not on a public blog post, or visitors who are not signed in will be blocked.

The public address looks like `/files/{slug}`. The slug is the short name in the link. Ask your agent if you want that link on a page. Do not guess slugs.

## What this is not

Files is not a full media library. Photos on the public homepage still live in the site files. Ask your agent to change those. See [Edit pages and content](./public-site.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/vocabulary.md -->

# Words on your site

The product does not hard-code “member”, “class”, “coach”, “client”, or “technician” in screens, URLs, or tables. Your site config supplies the words.

## What you can change

Three sets of words:

- **Customer** — member, client, customer
- **Session** — class, visit, appointment (for group sessions today)
- **Staff** — coach, technician, staff member

Ask your agent: “Call customers members, sessions classes, and staff coaches.” They edit `site/config/site.config.ts`. After merge, admin and the portal use those words.

## What stays the same

A few membership sentences still say **members**, including **Let members cancel online**. That is product copy, not your vocabulary.

URLs stay the same: `/admin/customers`, `/portal`, `/schedule`. Only the labels change.

Gyms vs trades is not a mode you pick. It is the starter you began from, plus which [features](./turning-features-on.md) you turned on.

---

<!-- source: https://www.xylosoftware.com/cms/docs/memberships.md -->

# Memberships and plans

Memberships are recurring plans you sell on your site. A customer with an open plan is a member in product language, not a separate kind of person. Money goes to **your** Stripe account.

This tool needs [payments](./payments.md) and the [customer portal](./customer-portal.md). Ask your agent to turn `memberships` on. You cannot flip that switch in admin.

## Add a membership plan

1. Finish [Stripe setup](./stripe.md) with **test** keys until your Stripe account is verified and a bank is connected.
2. Sign in as the owner. Staff can look, but only an owner can add a plan.
3. Open admin → **Catalog**.
4. Fill **Add a plan** on the right: name, description, price, and **How often** it bills.
5. Press **Add plan**.
6. Leave it offered (active) if customers should see it.

![Add a plan form in Catalog, with the form and Add plan button marked](./assets/admin-catalog-add-plan.webp)

Orange numbers:

1. The **Add a plan** form
2. **Add plan**

Plans must be at least $0.50.

## How often it bills

Presets on the form:

- Every 28 days
- Every 4 weeks
- Monthly
- Yearly

You can also set a custom interval: day, week, month, or year, with a count. Examples: every 14 days, every 3 months. A year can only be a count of 1. A day plan cannot be longer than 365 days.

To bill every 28 days, set **How often** to **Every 28 days**, then press **Add plan**.

![How often set to Every 28 days, with Add plan marked](./assets/admin-catalog-28-day.webp)

Orange numbers:

1. **How often**
2. **Add plan**

The catalog is the source of truth. Stripe product and price ids are a cache created when you save (and on first checkout). If you change the price or cadence, the old Stripe price is archived and a new one is created.

## Let members cancel online

Admin → **Memberships**. Owners see **Let members cancel online** and **Save**. It is on by default.

![Memberships settings, with the cancel-online checkbox and Save marked](./assets/admin-memberships.webp)

Orange numbers:

1. **Let members cancel online**
2. **Save**

When it is on, a signed-in customer can cancel from the billing portal (or the simulated manage page when you have no Stripe keys).

When it is off, they see a note to contact you. Staff can still press **Cancel at period end** on a row.

## How a customer joins

They must already be a [customer](./customers.md) who can sign in. They open `/portal/memberships` and start checkout for the plan you offered.

![Customer portal plan page](./assets/portal-memberships.webp)

Link that page from your public site if you want a public “join” path. They still have to sign in.

After Stripe confirms, the plan shows in admin. Do not trust the Stripe dashboard alone.

Without Stripe keys, local and preview deploys **simulate** a join. Nothing is charged. The row is marked simulated. In production with no keys, online signup is refused.

## What the statuses mean

| Status | Meaning |
| --- | --- |
| Incomplete | Checkout started, not finished |
| Trial | In a trial (if you use one) |
| Active | Paid up |
| Past due | A recurring payment failed. They are still a member, flagged. |
| Canceled | The plan has ended |
| Unpaid / paused | Still open, not in good standing |

A customer can have only one open plan at a time.

## What customers see

Portal home has a **Your plan** card. `/portal/memberships` shows the plan name, status, and cadence in plain words (monthly, every 28 days). **Manage billing** opens Stripe’s billing portal when Stripe is connected: update a card, see invoices, change plan, and cancel if you allow it.

## Messages you can send

Your follow-ups can react to:

- `membership.started`
- `membership.past_due`
- `membership.canceled`

Ask your agent to wire those in [messages](./messages.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/session-packs.md -->

# Session packs

Session packs are one-time packs of credits. A gym might sell a 10-class card. They are not a recurring plan. Recurring billing is [memberships](./memberships.md).

This tool needs [payments](./payments.md) and the [customer portal](./customer-portal.md). Ask your agent to turn `sessionPacks` on.

## Add a pack

1. Sign in as the owner.
2. Open admin → **Catalog**.
3. Fill **Add a pack** on the right: name, description, price, session credits, and optional expiry.
4. Press **Add pack**.

![Add a pack form in Catalog, with the form and Add pack marked](./assets/admin-catalog-add-pack.webp)

Orange numbers:

1. The **Add a pack** form
2. **Add pack**

Credits are 1 to 10,000. Expiry is days from the grant, 1 to 3,650, or none. No expiry means leftover credits do not run out.

Staff can look. Only an owner can change packs.

## How a customer buys

They sign in and open `/portal/session-packs`. Checkout is a one-time payment on **your** Stripe account.

Without Stripe keys, local and preview deploys simulate the purchase. Nothing is charged.

## How credits get used

- When group session booking is on, booking a session uses **one credit per place**.
- If staff add the person to a session, that also uses a credit.
- If booking is off, staff use **Check in / use credit** on the [customer](./customers.md) page.

If they cancel or you remove them from the session, the credit is put back. Expired lots do not count as available.

If session packs are off, group booking does not charge a credit.

## What you see

Admin → **Customers** → the person. You will see remaining credits and the next expiry, if any. You can **Update credits** (an admin grant) or check them in.

Portal home has a **Your packs** card.

## Refunds

A pack purchase is a [payment](./payments.md). Owners refund from admin → Payments. Hand the money back yourself for cash or a simulated payment.

---

<!-- source: https://www.xylosoftware.com/cms/docs/payments.md -->

# Payments

Payments is one-time money: cards through Stripe on **your** account, plus cash and checks staff record. Invoices, quote deposits, memberships, and session packs all go through this foundation. There is not a second ledger.

Ask your agent to turn `payments` on. You cannot flip that switch in admin.

![Payments list in the Brightside Studio admin](./assets/admin-payments.webp)

## Record cash or a check

Admin → **Payments**. Use **Record a payment** on the right. Choose how they paid (**Cash**, **Check**, or **Other**). Press **Record payment**. That is for money paid in person.

![Record a payment form, with the form and Record payment marked](./assets/admin-payments-record.webp)

Orange numbers:

1. The **Record a payment** form
2. **Record payment**

Online payments show as **card** or **ACH**. You do not type those in by hand. Stripe sends them after checkout.

## Refund a payment

Only an owner sees **Refund**. For a card or bank payment, Stripe refunds it. For cash, a check, or a simulated payment, the row is marked refunded. Hand the money back yourself.

## Pay links

Some features send a private link at `/pay/{token}`. The customer opens it and pays. Tips appear only when that feature allows them (invoices can turn tips on in config).

## What “Stripe is connected” means

The **Online payments** card on this page tells you the mode:

- **Stripe** — keys are set. Checkout is real (test or live, matching the keys).
- **Simulated** — no keys, and you are on a local or preview deploy. Presses succeed. Nothing is charged. Rows say **Test**.
- **Unavailable** — no keys in production. Pay links cannot take money online. Recording cash still works.

Set keys with [Stripe setup](./stripe.md). Use test keys until the account is verified and a bank is connected.

## Memberships vs this page

A membership renewal is still a Stripe subscription. You manage plans in Catalog and Memberships. This page is the payment list, including those charges once they land.

---

<!-- source: https://www.xylosoftware.com/cms/docs/invoices.md -->

# Invoices

Invoices are what a customer owes. A studio can invoice for a private session. A plumber can invoice for a visit. Invoices do not need a “jobs” tool.

Ask your agent to turn `invoices` on. Online pay on an invoice also needs [payments](./payments.md).

## Start an invoice

Admin → **Invoices**. Use **New invoice**. Pick the customer and the lines. Save a draft, then **Send invoice**.

![Invoices page with the new-invoice form marked](./assets/admin-invoices.webp)

![New invoice form with New invoice marked](./assets/admin-invoices-new.webp)

Orange numbers:

1. The **New invoice** form. Pick the customer.
2. Press **New invoice** to start a draft.

You can send several at once with **Select all to send** and **Send N selected**.

## What the statuses mean

| Status | Meaning |
| --- | --- |
| Draft | Only staff see it |
| Awaiting payment | Sent. The customer has a link |
| Past due | Still awaiting payment, after the due date |
| Paid | Paid online or recorded by staff |
| Bad debt | You gave up collecting |
| Void | Canceled |

Payment terms default to 14 days unless your agent set a different `paymentTermsDays`.

## Send the customer their link

On the invoice, **Copy customer link** or send it by email or text. The public page is `/i/{token}`. **PDF** downloads a copy.

With payments on, they can pay on that page. Staff can **Record a payment** if they paid in person.

## Other actions

- **Send again**
- **Mark bad debt**
- **Reopen**
- **Void**
- **Delete draft**

## Reminders

Overdue reminders go through [messages](./messages.md). Ask your agent to set `invoice.due` and `invoice.overdue` follow-ups.

## Tips

Invoice pay can offer a tip if `invoices.tips` is on in config. Ask your agent. The default is off.

---

<!-- source: https://www.xylosoftware.com/cms/docs/quotes.md -->

# Quotes

Quotes are priced offers. You send a link. The customer ticks optional lines, then signs, or sends a note asking for a change.

Ask your agent to turn `quotes` on. A deposit on approval needs [payments](./payments.md).

## Start a quote

Admin → **Quotes**. Press **New quote**, then **Start quote**. Add lines from the [catalog](./admin.md) or type a line. New quotes stay open for 30 days unless your agent set a different `validDays`.

![Quotes page with the new-quote form marked](./assets/admin-quotes.webp)

![Start quote form, with the form and Start quote marked](./assets/admin-quotes-new.webp)

Orange numbers:

1. The new-quote form. Pick the customer.
2. **Start quote**

## Send it

**Send quote** emails (and texts, if texting is on). **Copy link** if you want to paste it yourself. The customer opens `/q/{token}`. **PDF** is there too.

They approve with a typed name or a drawn signature. Or they send a note and the quote becomes **changes requested**.

## What the statuses mean

Draft → awaiting response ⇄ changes requested → approved → converted. You can also archive a quote, or delete a draft.

## After they approve

The quote page lists what you can convert it to (an invoice, for example), when those tools are on. **Mark approved** is for when they agreed off the link.

With payments on, they can pay a deposit on the quote link.

## Catalog items

Quote lines often come from admin → **Catalog**. Owners add items there. Staff can look. Only an owner can change prices.

---

<!-- source: https://www.xylosoftware.com/cms/docs/group-sessions.md -->

# Group sessions

Group sessions are times many people attend at once: a weekly class, a workshop, an open gym block. Staff run them in admin → **Schedule**. The public timetable is `/schedule`.

Ask your agent to turn `groupSessions` on. Customer self-booking also needs the portal and `groupSessionBooking`.

![Weekly schedule in the Brightside Studio admin, with the week grid and Weekly schedule link marked](./assets/admin-schedule.webp)

Orange numbers:

1. This week’s sessions
2. **Weekly schedule** — repeating slots live here

![Public timetable for Brightside Studio](./assets/public-schedule.webp)

On a phone:

![Public timetable on a phone](./assets/public-schedule-mobile.webp)

## Add a weekly session

Admin → **Schedule**. Open **Weekly schedule** (top right). Add the repeating slot there. Your site may say **class** instead of session.

![Weekly schedule editor, with the repeating slots marked](./assets/admin-schedule-weekly.webp)

To take a series off the week, remove it from the weekly schedule.

## Add a one-off session

From the week view, fill **Add a one-off session** and press **Add session**. That is a single date, not a repeating slot.

## Close a day

Fill **Close for a day**, then **Close this day**. Reopen from **Closed days** if plans change. **No closed days coming up** means none are set.

Move weeks with the arrows next to the date range.

## Who signed up

Open a session. You will see the roster. Staff can mark **They came** or **No-show**, **Remove** someone, or assign a staff member.

**Cancel session** takes it off. **Put it back on** restores it. You can **Delete** a one-off with an empty roster.

Staff can add a customer even if the session is full or in the past.

## Let customers book online

Turn on `groupSessionBooking` (needs group sessions and the portal). Customers then book at `/portal/sessions`.

Rules today:

- They can book this many days ahead: `bookAheadDays` in config (default 14). Ask your agent to change it.
- Sign-ups close shortly before the session starts. The cutoff is 15 minutes unless someone changes `group_session_settings` in the database. There is no admin screen for that yet. Ask your agent.

If session packs are on, a booking uses one credit. See [Session packs](./session-packs.md).

If booking is off, staff still run the schedule. Customers may see upcoming sessions on the portal home without a book button.

---

<!-- source: https://www.xylosoftware.com/cms/docs/scheduling.md -->

# Scheduling

Scheduling is the engine under the calendar. It is not a switch you turn on. [Group sessions](./group-sessions.md) use it today.

## What it already does

- Repeating weekly slots
- One-off sessions
- Staff assignees
- Closed days
- Conflict checks

## What is not shipped

Appointments (one customer at a time) and job visits are not in this version. When they exist, they will use this same engine. Do not ask the admin to “turn on appointments.” That screen is not there yet.

## Where you work

Admin → **Schedule**, when group sessions are on. The public page is `/schedule`.

![Weekly schedule editor in the Brightside Studio admin](./assets/admin-schedule-weekly.webp)

---

<!-- source: https://www.xylosoftware.com/cms/docs/turning-features-on.md -->

# Turn features on and off

Most tools are off until someone turns them on in site config. You cannot flip these switches in admin today. Ask your agent: “Turn on memberships” or “Turn off quotes.”

The generated [feature catalog](./features.md) is the machine list. This page is the owner list.

## Always on

These have no switch:

| What | Where you see it | Notes |
| --- | --- | --- |
| Public website | Your domain | Pages, blog, `/links`. [Edit pages and content](./public-site.md). |
| Admin | `/admin` | [A tour of admin](./admin.md). |
| Customers | Admin → Customers | [Customers](./customers.md). |
| Staff | Admin → Staff | Owner invites people. |
| Leads | Admin → Leads | Always creates leads from lead forms. Turning this off is still planned. |
| Scheduling engine | Used by group sessions | Not a menu item of its own. [Scheduling](./scheduling.md). |
| Message queue | Follow-up emails and texts | [Messages](./messages.md). |
| Site config | Not in admin | Features, words, forms, pipeline. Ask your agent. |

## Switches you can ask for

Every switch below defaults to **off**. If a tool needs another, the site will not start until both are on.

| Ask your agent to… | Config key | What you get | Also needs |
| --- | --- | --- | --- |
| Turn on the customer portal | `customerPortal` | Customers sign in at `/portal`. Staff invite them. | — |
| Turn on group sessions | `groupSessions` | Admin → Schedule, public `/schedule`. | — |
| Let customers book sessions | `groupSessionBooking` | `/portal/sessions`. Book-ahead days live in this switch (default 14). | group sessions, portal |
| Turn on memberships | `memberships` | Plans in Catalog, Memberships list, `/portal/memberships`. | payments, portal |
| Turn on session packs | `sessionPacks` | Packs in Catalog, credits, `/portal/session-packs`. | payments, portal |
| Turn on files | `files` | Admin → Files, `/files/{slug}` links. | — |
| Turn on payments | `payments` | Admin → Payments, pay links. | — |
| Turn on invoices | `invoices` | Admin → Invoices, `/i/{token}`. Payment terms (default 14 days) and tips (default off) live here. | payments, if they should pay online |
| Turn on quotes | `quotes` | Admin → Quotes, Catalog items, `/q/{token}`. Valid-for days (default 30) live here. | payments, if they should pay a deposit |

## Toggles that *are* in admin

| Toggle | Where | Who |
| --- | --- | --- |
| **Let members cancel online** | Admin → Memberships → Settings | Owner |
| File access **Anyone** vs **Signed-in only** | Admin → Files, per file | Staff |
| Plan or pack **offered** (active) | Admin → Catalog | Owner |

## Config knobs that are not a yes/no switch

Ask your agent to set these. They are not in admin.

| Setting | Default | Effect |
| --- | --- | --- |
| `groupSessionBooking.bookAheadDays` | 14 | How far ahead a customer can book |
| Booking cutoff minutes | 15 | Stored in the database. Sign-ups close this many minutes before start. No admin screen. |
| `invoices.paymentTermsDays` | 14 | Due date on new invoices |
| `invoices.tips` | off | Tip on invoice pay links |
| `quotes.validDays` | 30 | How long a new quote stays valid |
| `customerPortal.requestForm` | none | A “New request” button in the portal |
| Vocabulary | customer / session / staff member | [Words on your site](./vocabulary.md) |
| Pipeline stages and follow-ups | site CRM config | [Leads](./leads.md), [Messages](./messages.md) |

## Gym vs plumber

There is no “gym mode.” A gym starter turns on portal, group sessions, booking, memberships, packs, files, payments, and invoices. A trades starter turns on portal, files, payments, quotes, and invoices. You can ask for a different mix later, as long as the “also needs” column is satisfied.

---

<!-- source: https://www.xylosoftware.com/cms/docs/ask-your-agent.md -->

# What to ask your agent

Your AI agent works in your GitHub repo. Speak like you would to a capable assistant. Short sentences. Real facts. The agent should open a pull request, show you the change, and wait for you to merge.

## Pages and photos

- “Change the homepage headline to ‘Open 6am to 8pm’.”
- “Swap the hero photo for this image.”
- “Add a page that explains how personal training works. Use only the copy I paste.”
- “Put this Instagram caption and photo on the blog.”
- “Add our address and phone to the footer. Phone is (614) 555-0123.”
- “Redirect /old-class-pass to /packs.”

Do not let it invent prices, bios, testimonials, or results. If you did not supply the fact, it should ask.

## Leads and follow-ups

- “Rename the pipeline stage Booked to Trial booked.”
- “Add a program called On-site visits.”
- “Wait three days before the second follow-up text, and change the wording to …”
- “Stop texting people who are already in Won.”

## Plans, packs, and money

- “Turn on memberships.”
- “Add a plan called Unlimited, $149 every 28 days.”
- “Add a 10-class pack for $120 that expires in 90 days.”
- “Turn off Let members cancel online.” (You can also do that yourself in admin → Memberships.)
- “Turn on invoices, with 14-day terms and no tips.”
- “Turn on quotes.”
- “Recorded payments should stay; just help me connect Stripe in test mode.” Then point them at [Stripe setup](./stripe.md).

## Schedule and portal

- “Turn on group sessions and the public timetable.”
- “Let members book online, 14 days ahead.”
- “Invite Maria from the customer list to sign in.” (You can also do that in admin.)
- “Call customers members, sessions classes, and staff coaches.”

## Files

- “I uploaded a waiver in admin → Files. Put the signed-in-only link on the portal home.”

Give the slug from **Copy link**. Do not ask the agent to guess it.

## Going live (technical)

These belong in the Technical section. Ask only when you mean it:

- “Walk me through Stripe webhook setup for YOUR-DOMAIN.”
- “Add the production env vars listed in hosting docs. Do not print secrets.”
- “After we merge, confirm the Vercel production deploy succeeded.”

## What the agent will not do (unless you clearly ask)

- Dispatch the production database migration workflow, or approve a waiting production deploy
- Merge a change without a pull request
- Put secret keys in the repo
- Invent business facts
- Change the public `/cms` marketing page on xylosoftware.dev
- Edit `node_modules` or generated sync files by hand

## What it should check with you first

- Prices, class times, staff bios, testimonials, and photos of real people
- Turning a feature **off** that you already use (booking, memberships, invoices)
- Anything that emails or texts your real customers
- Ejecting (leaving Xylo). That is one-way. See [Leave Xylo](./eject.md).

## How a change goes live

1. You ask.
2. The agent opens a pull request.
3. You read it (and a local preview, if they ran one).
4. You merge with a merge commit.
5. Vercel deploys `main`. If the change included database files, migrations apply automatically after a backup. See [Backups and database changes](./backups.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/messages.md -->

# Messages

The CMS does not invent a separate email product. Features emit events. Your site config says which emails and texts to send. A scheduled job sends what is due about every 15 minutes.

## What you can ask your agent to change

- Wording and timing of lead follow-ups
- Who gets pinged about a new lead (`STAFF_NOTIFY_EMAILS`, `STAFF_NOTIFY_PHONES`)
- Automations for events such as `quote.sent`, `invoice.due`, `membership.started`

There is no admin screen for this yet. See [What to ask your agent](./ask-your-agent.md).

## Texts need opt-in

Never text a lead who did not tick the consent box. Keep that wording in line with `/terms-of-service`. Until Twilio is set up, texts are logged as simulated and everything else still works.

## Local testing

On a developer machine, emails land in Mailpit instead of a real inbox. That is expected.

## What this is not

This is not a marketing newsletter tool. It is follow-ups and receipts tied to leads, quotes, invoices, and plans.

---

<!-- source: https://www.xylosoftware.com/cms/docs/faq.md -->

# FAQ

Short answers for owners. If a fix is technical, the link goes to the Technical section.

## I cannot sign in

Use `https://YOUR-DOMAIN/sign-in`. Try the emailed code if the password fails. If you see “no access”, you are not on the staff list. The owner invites you from admin → Staff. See [Sign in to admin](./getting-started.md).

## I do not see Schedule / Memberships / Payments

That tool is off. The menu only shows what is on. Ask your agent to turn the switch on. See [Turn features on and off](./turning-features-on.md).

## Can I change homepage copy in admin?

Not today. Ask your agent. See [Edit pages and content](./public-site.md).

## How do I add a membership plan?

Owner, admin → Catalog → **Add a plan**. Pick a cadence, including every 28 days. See [Memberships and plans](./memberships.md).

## How do I sell a 10-class pack?

Owner, admin → Catalog → **Add a pack**. See [Session packs](./session-packs.md).

## How do I let members cancel online?

Admin → Memberships. Owners see **Let members cancel online**. Save. Staff cannot change it.

## A customer paid but admin still says unpaid

For live Stripe, wait for the webhook, then refresh. Confirm the event in Stripe → Webhooks shows 200. See [Stripe setup](./stripe.md). Do not mark it paid by hand unless they paid cash.

## Pay links say online payment is not set up

Production has no Stripe keys. Staff can still record cash. Connect Stripe with test keys first.

## Someone filled a form and got an error, not a thank-you

The site may have caught a spam trap. Ask them to email or call you. Autofill can tick a hidden field.

## Texts are not going out

Until Twilio is approved, texts are simulated. Consent must be ticked. See [Messages](./messages.md).

## I merged a change and the site looks old

Hard-refresh. If it still looks old, check the Vercel production deploy for that commit. Ask your agent to confirm. See [Hosting and deploys](./hosting.md).

## Do I need to run a database command when we merge?

No. On a live site repo, merging migration files to `main` backs up, rehearses, then applies them. See [Backups and database changes](./backups.md).

## How do I get an update from Xylo?

A version bump pull request. Your agent runs `pnpm xylo sync`, checks it, and you merge. See [Updates](./sync.md).

## Can I leave Xylo later?

Yes, by ejecting. It is one-way. You keep the code. You get no more updates. See [Leave Xylo](./eject.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/hosting.md -->

# Hosting and deploys

This page is for the person who hosts the site, or for your AI agent. Most owners never need it.

> **Caution.** Wrong environment values can take the site offline or send email from the wrong address. Back up first. Do not paste secret keys into chat logs. Ask your agent to do this with you.

## Who should do this

- Turning on a domain
- Setting environment variables on Vercel (or another host)
- First-time Supabase, Resend, or Twilio setup

Owners still click merge. The agent should not dispatch production database jobs unless you say so.

## Where the site runs

Vercel is the usual host. Merging to `main` deploys production. There are no pull-request preview deploys.

The app also runs on any host that can run Node 24 with `pnpm build` and `pnpm start`. Details for Docker and other hosts live in the internal `docs/HOSTING.md` in the product repo. The environment **names** are the same everywhere.

Health check: `GET /api/health` returns `{"ok":true}`.

## Environment variables

Set these on the host (Vercel → Environment Variables). Names:

| Name | What it is |
| --- | --- |
| `NEXT_PUBLIC_SITE_URL` | Public address, such as `https://www.example.com`. Baked in at **build** time. |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL. Build time. |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | Supabase publishable key. Build time. |
| `SUPABASE_SECRET_KEY` | Supabase secret key. Runtime. |
| `NPM_TOKEN` | GitHub token with `read:packages`, so the build can install `@xylo/core`. Never commit it. |
| `RESEND_API_KEY` | Email. |
| `EMAIL_SENDER` | From-address, such as `Brightside Studio <hello@example.com>`. |
| `STAFF_NOTIFY_EMAILS` | Who hears about new leads, comma-separated. |
| `STAFF_NOTIFY_PHONES` | Staff mobiles in E.164 (`+1…`), comma-separated. |
| `NEXT_PUBLIC_SMS_ENABLED` | `true` only after Twilio is approved. Build time. |
| `TWILIO_ACCOUNT_SID` / `TWILIO_AUTH_TOKEN` | Texting. |
| `TWILIO_MESSAGING_SERVICE_SID` | `MG…`, preferred. |
| `TWILIO_FROM_NUMBER` | Only if you are not using a messaging service. |
| `CRON_SECRET` | Random, 32+ characters. Used by the follow-up job. |
| `STRIPE_SECRET_KEY` | `sk_test_…` or `sk_live_…`. See [Stripe setup](./stripe.md). |
| `STRIPE_WEBHOOK_SECRET` | `whsec_…`. |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | `pk_…`. Build time. |
| `NEXT_PUBLIC_CAPTCHA_SITE_KEY` / `CAPTCHA_SECRET_TOKEN` | Optional Turnstile, only if spam gets through. |
| `CLIENT_IP_HEADER` | Optional. Host’s visitor-IP header. |
| `XYLO_PUBLIC_SUPABASE_URL` | Optional. Only if phones cannot use the public Supabase URL. |
| `POWERSYNC_URL` | Optional. Only if the native app syncs offline. |

Leave Twilio and Stripe **unset on Preview**, so preview URLs never text real people or take real money.

`NEXT_PUBLIC_*` values are baked in at build time. Changing one means rebuild (a new deploy).

## What a merge does

1. You merge the pull request to `main` with a merge commit.
2. Vercel builds and deploys.
3. If the pull request added files under `supabase/migrations/`, [migrations](./backups.md) start on their own: backup, rehearse, then apply.

Code can go live a minute before migrations finish. New code is written to work with the old schema for that window.

## First-time launch extras

The full launch runbook is internal (`docs/LAUNCH.md`). In short: create the owner’s Supabase project, turn off public sign-up, set site URL and `/auth/confirm`, paste the email templates, add Resend, then deploy. Invite the first owner with `pnpm owner:invite`. Everyone else is invited from admin → Staff.

## Follow-up job

Supabase `pg_cron` calls `/api/cron/nurture` every 15 minutes with `CRON_SECRET`. Vercel also calls it once a day as a fallback. Backups do **not** run on Vercel. They run in GitHub Actions. See [Backups and database changes](./backups.md).

---

<!-- source: https://www.xylosoftware.com/cms/docs/stripe.md -->

# Stripe setup

Xylo CMS bills through **the business owner’s own Stripe account**. There is no Stripe Connect and no Xylo platform fee. Money does not pass through Xylo.

> **Caution.** This is for the owner and their agent together. Live keys take real money. Use test keys until the Stripe account is verified and a bank is connected. Do not put secret keys in site config or in the Git repo. Back up nothing here except the knowledge that a wrong webhook URL silently drops payment events.

Until this is done, [payments](./payments.md), [memberships](./memberships.md) and [session packs](./session-packs.md) run in **simulated mode** locally and on preview deploys. Production pay links say online payment is not set up. Staff can still record cash and checks.

## Activate the account

Create or sign in at the Stripe dashboard and finish activating it: business details, bank account for payouts, identity. Stripe asks for these. Nobody else should fill them in.

## Keys

In Vercel (Production only; leave Stripe unset on Preview):

- `STRIPE_SECRET_KEY` — `sk_test_…` / `sk_live_…`, or a restricted key `rk_…`
- `STRIPE_WEBHOOK_SECRET` — `whsec_…` from the webhook endpoint
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — `pk_…` (optional for Checkout today; needed later for cards on file)

A restricted key needs **Write** on Checkout Sessions, Refunds, Customers, Products, Prices, Subscriptions, and Billing Portal Sessions/Configurations, and **Read** on PaymentIntents, PaymentMethods and Invoices. If a test payment fails with a Stripe permission error, add the permission the log names.

## Webhook

Developers → Webhooks → Add destination. Endpoint:

`https://YOUR-DOMAIN/api/stripe/webhook`

Subscribe to exactly these events (the CMS ignores anything else):

- `checkout.session.completed`
- `checkout.session.async_payment_succeeded`
- `checkout.session.async_payment_failed`
- `charge.refunded`
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `invoice.paid`
- `invoice.payment_failed`

Subscription events are used when memberships is on. They are harmless if it is off. Events are stored and applied **once**. Replays from the Stripe dashboard should not double-pay or double-activate.

## Billing portal

In Stripe, enable the Customer billing portal. The CMS opens a portal session for the signed-in customer’s own Stripe customer:

- Payment method updates and invoice history stay on
- Cancellation follows **Let members cancel online** on admin → Memberships (on by default). When that is off, the portal configuration has cancellation disabled.

Use **test** keys while you try this. The no-cancel configuration is created in Stripe the first time it is needed, then cached.

## Payment methods

Cards and wallets are on by default. Turn on ACH Direct Debit in Stripe if you want US bank payments (cheaper for large invoices; they take a few business days). Checkout offers whatever you enabled.

## Test then live

1. Test-mode keys and a test-mode webhook (its own `whsec_…`).
2. Buy a [membership](./memberships.md) plan, a [session pack](./session-packs.md) if that switch is on, and pay a test [invoice](./invoices.md) (card `4242 4242 4242 4242`).
3. Confirm status in admin after the webhook, not only in the Stripe dashboard.
4. Switch to live keys and a live webhook before launch.
5. Make a small real payment, confirm it in admin → Payments, then refund it there.

Payouts (daily by default, instant payouts to a debit card) are in the Stripe dashboard, not in the CMS.

---

<!-- source: https://www.xylosoftware.com/cms/docs/backups.md -->

# Backups and database changes

This page is for the person who holds the GitHub and Supabase accounts, or for your AI agent.

> **Caution.** Restoring a backup overwrites data. Applying migrations changes the live database. Both are easy to get wrong. The nightly backup and the merge-time backup are automatic. Do not dispatch the migration workflow by hand unless the owner asked. Keep the backup private key offline. If you lose it, old backups cannot be read.

## Nightly backups

Each live site repo has a GitHub Action (`backup.yml`) that writes an encrypted dump of the database every night at 07:15 UTC, kept 7 days.

It needs repository secrets `SUPABASE_ACCESS_TOKEN`, `SUPABASE_DB_PASSWORD`, and `SUPABASE_PROJECT_REF`, plus a variable `BACKUP_AGE_RECIPIENT` (`age1…`). Generate that key pair with `age-keygen`. Keep the private identity offline. Never add the identity to GitHub.

Check now and then that **Actions → Nightly database backup** is green.

## Migrations apply when you merge

On a **site** repo (not xylo-cms itself), merging files under `supabase/migrations/` to `main` starts `migrate.yml`:

1. Backup production (kept 30 days)
2. Rehearse on a throwaway database on the runner
3. Backup again, then `supabase db push` to production

That merge is the approval. New code may go live on Vercel while the migrate run is still rehearsing. Migrations are written so the old schema still works for that window.

Until the three Supabase secrets are set, the workflow logs a warning and applies nothing. Once they are set, it also needs `BACKUP_AGE_RECIPIENT` and fails without it: no backup, no migrations.

AI agents do not dispatch this workflow, or approve a waiting production-environment deployment, unless the owner says to.

The very first push to an empty project, from a trusted machine:

```bash
supabase link --project-ref PROJECTREF
supabase db push
```

Use the real project ref. Do not write it with angle brackets.

## Restore

Download the artifact from Actions, decrypt it with the saved identity, unpack it, then load the SQL files into a new empty Supabase project, in order: `roles.sql`, then `schema.sql` + `data.sql` + history files in one transaction. Errors from `roles.sql` about Supabase’s own roles can be ignored. Expired artifacts cannot be recovered. The private key is required.

---

<!-- source: https://www.xylosoftware.com/cms/docs/sync.md -->

# Updates

This page is how a live site gets a new version of the product. It is not something you do every week from admin.

> **Caution.** An update is a pull request in **your** repo. Merging it deploys. If the release includes database files, [migrations](./backups.md) run after a backup. Let your agent run the checks. Do not skip `pnpm xylo sync`.

## What an update looks like

Core is a package. Your repo depends on a version of `@xylo/core`. A new version arrives as a pull request (from Renovate, or opened by hand).

Your agent should:

```bash
pnpm install
pnpm xylo sync
```

If sync wrote files, they commit them, then run:

```bash
pnpm xylo sync --check
pnpm typecheck
pnpm test
```

They paste the output on the pull request. You merge with a merge commit. Vercel deploys.

`pnpm xylo sync` rewrites generated files (routes, workflows, skills) from the installed core. It does not overwrite your `site/` content.

## Patch, minor, major

- **Patch** — a fix. Nothing for you to do except merge.
- **Minor** — something new, or a new setting with a default. The site keeps working. You may want to turn the new thing on.
- **Major** — your site files have to change too. The notes say `Site changes needed:`. The agent makes those on the same branch before you merge.

## Token for installs

Installing `@xylo/core` needs a GitHub token with `read:packages` in the **user-level** `~/.npmrc`, never in the repo. Vercel uses an `NPM_TOKEN` environment variable. `pnpm xylo sync --check` fails if a token line sneaks into the repo `.npmrc`.

## This docs site

The pages you are reading are exported from xylo-cms and copied into xylosoftware.dev as `cms-docs/`. Feature copy is updated in xylo-cms, then published here. Do not hand-edit `cms-docs/` except when reviewing that publish pull request.

Older docs called this page “Keeping docs current.” The owner-facing job is the version bump above.

---

<!-- source: https://www.xylosoftware.com/cms/docs/eject.md -->

# Leave Xylo

Eject copies the product into your repo so you no longer depend on Xylo’s private packages. It is the honest answer to “what if we leave.”

> **Caution.** This is one-way. After eject you get no updates, no warranty, and no help from Xylo. It needs a clean git tree. Commit what it leaves. Do not run this because you are curious. Back up the repo and the database first.

## Who should do this

Only the owner, with their agent, after a clear decision to stop taking Xylo updates.

## What the command does

In the **business repo** (not the demo app):

```bash
pnpm xylo eject --yes
```

It:

1. Runs `pnpm xylo sync` so generated files match the installed core
2. Copies core into the repo (routes, library, components, tests)
3. Rewrites imports so they no longer point at `@xylo/core`
4. Removes the package dependency and registry auth
5. Leaves a note at the top of `README.md`

It refuses to run without `--yes`, or if the working tree is dirty, or if a path it would copy is already your own file.

## What you keep

Your site, your admin, your database, your Stripe account, your customers. The app still builds with `pnpm install` and no private registry.

## What you lose

New features and fixes from Xylo. The `pnpm xylo` command. Help.

This is not a maintained path. If you only wanted a backup, use [nightly backups](./backups.md) instead.

---

<!-- source: https://www.xylosoftware.com/cms/docs/features.md -->

# Feature catalog

This list is generated from the product, so it cannot drift from the code. For a plain-language guide to each switch, see [Turn features on and off](./turning-features-on.md).

Machine-readable twin: [`catalog.json`](/cms/docs/catalog.json). Feature keys are the names in `site/config/features.config.ts`. Ask your agent to turn a key on or off. You cannot flip these in admin today.

- **shipped** — in use on live sites
- **current** — also in use (memberships and session packs)

Leads, the blog, the link-in-bio page, follow-up messages, and texting are **not** switches. See [leads](./leads.md), [public website](./public-site.md), and [messages](./messages.md).

## Always on

These have no `defineFeatures` switch. They are the platform.

| Key | Name | Owner doc | What it is |
| --- | --- | --- | --- |
| `customers` | Customers | [customers](./customers.md) | One record for everyone the business serves. Locations, tags, custom fields. A won lead becomes a customer. “Member” means a customer with an open membership, not a separate kind of person. |
| `staff` | Staff | [admin](./admin.md) | People with a staff row can sign in to admin (owner or staff). Sessions assign to staff. Owners invite staff and refund payments. |
| `leads` | Leads | [leads](./leads.md) | The enquiry pipeline. Not a feature switch today: lead forms always create leads, and admin always has the board. Turning leads off so forms only email the owner is still planned. |
| `scheduling` | Scheduling | [scheduling](./scheduling.md) | Recurrence, staff assignees, availability, closures and conflict checks. Group sessions use this engine. There is no appointments or jobs feature in this commit. |
| `messageQueue` | Message queue | [messages](./messages.md) | Features emit events (quote.sent, invoice.due, membership.started, …). The site’s automations and lead sequences say what to send. /api/cron/nurture sends what is due. |
| `siteConfig` | Site config | [vocabulary](./vocabulary.md) | Business details, vocabulary, forms, CRM follow-ups and feature switches live in site/config. An owner Settings page in admin is still planned. |
| `admin` | Admin | [admin](./admin.md) | The signed-in staff area at /admin. |
| `publicSite` | Public website | [public-site](./public-site.md) | Static public pages, blog posts, the link-in-bio page, and public forms. No feature switch: every site has them. Optional bits (phone, address, schedule) render nothing when missing. |

## Switches

Every key below is declared in `FEATURE_DEFAULTS` and is off unless the site turns it on.

| Key | Name | Maturity | Owner doc | Needs | Admin | Public |
| --- | --- | --- | --- | --- | --- | --- |
| `customerPortal` | Customer portal | shipped | [customer-portal](./customer-portal.md) | — | Customers (invite) | `/portal`; `/portal/account`; `/sign-in` |
| `groupSessions` | Group sessions | shipped | [group-sessions](./group-sessions.md) | — | /admin/schedule | `/schedule` |
| `groupSessionBooking` | Group session booking | shipped | [group-sessions](./group-sessions.md) | `groupSessions`, `customerPortal` | /admin/schedule (add a customer to a session) | `/portal/sessions` |
| `memberships` | Memberships | current | [memberships](./memberships.md) | `payments`, `customerPortal` | /admin/catalog (plans); /admin/memberships | `/portal/memberships`; `/portal/memberships/manage` |
| `sessionPacks` | Session packs | current | [session-packs](./session-packs.md) | `payments`, `customerPortal` | /admin/catalog (packs); /admin/customers (balance) | `/portal/session-packs` |
| `files` | Files | shipped | [files](./files.md) | — | /admin/files | `/files/{slug}` |
| `payments` | Payments | shipped | [payments](./payments.md) | — | /admin/payments | `/pay/{token}` |
| `invoices` | Invoices | shipped | [invoices](./invoices.md) | — | /admin/invoices | `/i/{token}`; `/i/{token}/pdf` |
| `quotes` | Quotes | shipped | [quotes](./quotes.md) | — | /admin/quotes; /admin/catalog | `/q/{token}`; `/q/{token}/pdf` |

## Collections

Table names in Postgres (`public`). Memberships use `membership_plans` and `memberships`, not a table named `subscriptions`.

| Collection | Summary | Used by |
| --- | --- | --- |
| `customers` | The person or organisation every feature hangs off: name, emails, phones, locations, tags, and site-defined custom fields. A lead becomes a customer when it is won. | `memberships`, `sessionPacks`, `customerPortal`, `quotes`, `invoices`, `payments`, `groupSessions`, `groupSessionBooking` |
| `staff` | People assigned to sessions. Staff availability and time off feed the scheduling engine. A staff row is also what lets someone sign in to admin. | `groupSessions` |
| `membership_plans` | Admin catalog of recurring offers (name, amount in cents, month or year). Checkout uses these plans; the browser is not trusted to supply Stripe product or price IDs. | `memberships` |
| `memberships` | Local subscription and access state for one customer’s plan. Status names match Stripe. One open membership per customer. Not a table named subscriptions. | `memberships` |
| `session_packs` | Admin catalog of one-time session-credit packs (name, amount in cents, credit count, optional expiry in days). Checkout uses these packs; the browser is not trusted to supply Stripe product or price IDs. | `sessionPacks` |
| `session_pack_purchases` | A customer’s one-time pack Checkout (or simulated purchase). Credits are granted once on paid Checkout. | `sessionPacks` |
| `session_pack_lots` | A grant of session credits with remaining balance and optional expiry. Available balance is unexpired remaining. | `sessionPacks` |
| `session_pack_credits` | Ledger of grants and uses (purchase, admin grant, booking use, deduct, reverse, refund). | `sessionPacks` |
| `catalog_items` | Products and services used as quote and invoice line items. Owners edit them on admin → Catalog, next to membership plans and session packs. | `quotes`, `invoices` |
| `quotes` | Priced offers with optional line items, tax, deposit, approval signature, and PDF. | `quotes`, `customerPortal`, `payments` |
| `invoices` | Amounts in integer cents, one currency per site, document numbers such as INV-1001. Stored statuses: draft, awaiting_payment, paid, bad_debt, void. past_due is computed. | `invoices`, `payments`, `customerPortal` |
| `payments` | One-time payment records. Stripe events are logged idempotently in stripe_events. Memberships reuse this foundation rather than a second ledger. | `payments`, `invoices`, `quotes`, `memberships`, `sessionPacks` |
| `scheduled_messages` | Queued email and SMS pointed at a lead, customer, quote, invoice, membership or session pack, with consent and quiet-hour safeguards. | `invoices`, `quotes`, `memberships`, `sessionPacks` |
| `site_files` | Uploaded files served at /files/{slug}. Access is public or signed-in customers (and staff). | `files` |
| `public_links` | Hashed tokens for /q/{token}, /i/{token} and /pay/{token}. The server checks the token before reading the record. | `quotes`, `invoices`, `payments` |
| `leads` | Enquiry pipeline from public forms. Stages and programs are the site’s CRM config. | — |
| `schedule_occurrences` | Dated instances of the scheduling engine (group sessions today). Capacity, cancel state, staff assignees. | `groupSessions`, `groupSessionBooking` |
| `group_session_bookings` | A customer booked on a group-session occurrence. | `groupSessionBooking` |
| `tax_rates` | Named tax rates in basis points. One default. Copied onto quotes and invoices. | `quotes`, `invoices` |

## Routes in this commit

### Admin

- `/admin`
- `/admin/account`
- `/admin/catalog`
- `/admin/customers`
- `/admin/customers/:customerId`
- `/admin/files`
- `/admin/invoices`
- `/admin/invoices/:invoiceId`
- `/admin/invoices/:invoiceId/pdf`
- `/admin/leads`
- `/admin/leads/:id`
- `/admin/memberships`
- `/admin/payments`
- `/admin/quotes`
- `/admin/quotes/:quoteId`
- `/admin/quotes/:quoteId/pdf`
- `/admin/schedule`
- `/admin/schedule/:sessionId`
- `/admin/schedule/weekly`
- `/admin/staff`

### Customer portal

- `/portal`
- `/portal/account`
- `/portal/invoices/:invoiceId`
- `/portal/memberships`
- `/portal/memberships/manage`
- `/portal/request`
- `/portal/session-packs`
- `/portal/sessions`

### Public token links

- `/files/:slug`
- `/i/:token`
- `/i/:token/pdf`
- `/pay/:token`
- `/q/:token`
- `/q/:token/pdf`

### Sign-in

- `/auth/confirm`
- `/forgot-password`
- `/sign-in`
- `/update-password`

### Public site and forms

- `/`
- `/.well-known/xylo.json`
- `/about`
- `/blog`
- `/blog/:slug`
- `/blog/page/:page`
- `/cancel-plan`
- `/forms/:form`
- `/get-started`
- `/links`
- `/privacy-policy`
- `/request-visit`
- `/schedule`
- `/services`
- `/services/:slug`
- `/terms-of-service`

---

<!-- source: https://www.xylosoftware.com/cms/docs/for-agents.md -->

# For agents

This docs set is meant to be read by language models and coding agents as easily as by people. Owners should start at [What your site can do](./index.md).

HTML is at `/cms/docs`. Every page has a Markdown twin at `/cms/docs/{page}.md`. Start with [`llms.txt`](/cms/docs/llms.txt). Full dump: [`llms-full.txt`](/cms/docs/llms-full.txt). Feature catalog JSON: [`catalog.json`](/cms/docs/catalog.json).

## How to behave

- Speak in plain words to the owner. They run a gym or a trades business.
- Open a pull request. Do not push to `main`.
- Do not invent prices, bios, testimonials, or results.
- Do not print secrets.
- Do not dispatch `migrate.yml` or approve a production deploy unless the owner asked.
- Update `docs/public/` in the **same** PR when owner-facing behaviour changes.
- Run `pnpm docs:check` on xylo-cms changes.

## Placeholders

Never write angle-bracket placeholders. The public site treats those as HTML tags. Use `YOUR-DOMAIN` or `{slug}` in inline code.

## Heading anchors

Anchors are stable slugs from heading text: lowercase, punctuation to hyphens, duplicates get `-2`. Prefer not to rename H2s that other pages link to.

## Skills in a site repo

Shipped with `pnpm xylo sync`:

- `add-blog-post`
- `update-site-content`
- `edit-follow-ups`
- `link-a-file`
- `apply-site-update` (legacy git-merge updates; package bumps use [Updates](./sync.md))

## Screenshots

Owner docs may include a Markdown image pointing at `docs/public/assets/` (for example `admin-dashboard.webp`) from the seeded Brightside Studio demo only. No live customer data.

---

<!-- source: https://www.xylosoftware.com/cms/docs/product.md -->

# The Xylo CMS offering

This is the docs twin of the public [Xylo CMS page](/cms). That page is the offering. These docs are the how-to for people who already have a site.

Xylo CMS is one business’s website plus the tools to run it: pages the public sees, an admin for staff, customers as one record, and optional billing in the owner’s own Stripe account.

Gyms, trades, and other kinds of business are starters on the same product, not separate products. You turn tools on by what they do (memberships, quotes, invoices), not by picking a “gym mode.”

Xylo does not take phone or web-form sign-up for the CMS. Email xylo@xylosoftware.dev.

If you already run a site, start at [What your site can do](./index.md).

---
