> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arcsolar.com.au/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> ## What this documentation covers
> This is the documentation for ArcSolar, the solar-retailer operating system: the web app a solar retailer uses to run quoting, sales, installation, rebates, inventory and reporting. ArcSolar is a hosted web application at https://app.arcsolar.com.au. There is no public API for most workflows, so agentic operation of ArcSolar is UI-driven. Navigate the app, read the labels and controls, and act through the interface, unless a page explicitly documents an API, contract or integration endpoint.
>
> ## How to read these pages
> - Pages are split by audience using Mintlify's Visibility component, and this split is deliberate. Sections marked 'for agents' are hidden on the website and appear only in the Markdown you are reading. They are the authoritative behavioural specification for the page. Where the remaining human-facing prose and an agent section disagree, the agent section is correct.
> - Sections outside any Visibility block appear in both outputs. They are the shared spine of the page: what the feature is and why it matters.
> - Treat every bold label, field name, route, status string and error message quoted in an agent section as canonical. They are copied verbatim from the product. Use that exact wording when telling a user where to click, or when matching an error message to its cause.
> - Australian solar vocabulary is used precisely and not interchangeably: STC (Small-scale Technology Certificate), PRC (Peak Reduction Certificate), REPS, NMI, CEC accreditation, VPP. Do not substitute an overseas equivalent.
> - Money is Australian dollars. Dates shown to users are day-first (DD/MM/YYYY).
>
> ## Behaviour rules for operating or advising on ArcSolar
> - Respect prerequisites and ordering. Agent sections state which inputs block progress and which steps must happen first. Do not skip a step, and do not assume a default that the product itself does not supply.
> - Never invent a value the product requires but the user has not supplied. Ask for mandatory fields rather than guessing. Several features reject a request outright when one is missing, and submissions to external authorities are effectively irreversible.
> - Respect permissions and scoping. Features name the roles, capabilities and access rules that may act. Do not attempt an action the operator's role cannot perform, and do not describe a restricted feature as available to them.
> - Features that are gated, pending release, or blocked on a vendor are documented as exactly that. Never present a gated or unmerged behaviour as live. When availability is unclear for a given workspace, direct the user to support rather than asserting that the feature works.
> - Where a page states something is genuinely undetermined, treat it as unknown. Do not fill the gap with a plausible guess.
> - ArcSolar's built-in assistant is called ArgonixIntelligence. Pages end with an 'ArgonixIntelligence in this workflow' section stating what intelligence can and cannot reason from in that workflow. Treat those limits as binding: intelligence does not replace provider approval, verified evidence, or a customer's own confirmation, and it must not be presented as if it does.
>
> ## Getting help
> Support is support@arcsolar.com.au. The customer-facing website is https://www.arcsolar.com.au.

# Roof design integration contract

> The API contract, asset specification, permissions and data shapes behind Roof Design.

This page is the technical contract behind [Roof Design](/quoting/roof-design). It is written for integrators and AI agents that need to operate or reason about the feature precisely. Contract version: **1**.

**Posture.** The editor is a separate first-party application embedded in an iframe, launched from inside a quote. It is not a public API and it is not documented for third-party integration. Treat every path below as internal.

**API base.** `/api/roof-design/v1`, served by a catch-all route on the Node.js runtime. Responses set `Cache-Control: no-store`, `Referrer-Policy: no-referrer` and `Vary: Origin`. CORS echoes the configured roof design origin only, with credentials allowed and headers `Authorization, Content-Type`.

**Request size limit.** Any request body over roughly 5 MB is refused with `422 VALIDATION` — `Request too large.` A design document over 5 MB is refused with `Design document exceeds 5 MB.`

### Authentication modes

| Mode              | How                                                                  | Used by                                          |
| ----------------- | -------------------------------------------------------------------- | ------------------------------------------------ |
| Cookie session    | The signed-in ArcSolar session                                       | `launch`, `attach`, and cookie-authorised writes |
| Bearer capability | `Authorization: Bearer <accessToken>`, a project-scoped opaque token | Embedded-app reads and writes                    |

Rules that matter:

* `launch` and `attach` require cookie identity and **reject** a bearer token: `403 Launch from ArcSolar.` and `403 Review in ArcSolar.`
* Cookie-authorised writes are accepted only when the request origin equals the app origin or the configured roof design origin, otherwise `403 Request origin denied.`
* Cookie writes additionally require the `quote.revise` capability. A denied capability surfaces as `403 You do not have permission to edit quote designs.`
* `sign-out` is origin-gated and returns `204`.
* The bearer capability is workspace- and project-scoped. Every project, revision, asset and command query filters on the workspace, and a request naming any other project returns `403 This launch is restricted to its project.` A capability can never create a project: `403 Open a new project from ArcSolar.`

### Launch and exchange

| Step     | Route                               | Detail                                                                                                                                                                                                                  |
| -------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Launch   | `POST /api/roof-design/v1/launch`   | Accepts an address, optional coordinates, and optional project, customer, site, opportunity, quote draft id and draft key. Creates a project when none is given, then loads it to enforce access before issuing a code. |
| Exchange | `POST /api/roof-design/v1/exchange` | Body is a single `launchCode`, 40 to 100 characters. Returns an access token, its expiry, the project id, the organisation id, the quote draft id.                                                                      |
| Sign out | `POST /api/roof-design/v1/sign-out` | Origin-gated; ends the embedded session locally.                                                                                                                                                                        |

The launch code is bound to the exact origin, expires in **60 seconds**, and is consumed once atomically. A second exchange of the same code fails with `401 LAUNCH_EXPIRED` — `This launch expired or was already used. Reopen from ArcSolar.` The access token is the opaque capability itself, not a signed token, and it is valid for at most **10 minutes**, capped by the remaining ArcSolar session. If the session has under a minute left, launch is refused with `401 UNAUTHENTICATED` — `Refresh your ArcSolar session before opening the panel.`

The launch response omits extracted credentials from any URL. Nothing in this flow puts a credential in a query string, a fragment, or a referrer.

### Design revisions and assets

A revision is an immutable checkpoint. Creating one copies the authoritative project context and selected equipment — never a snapshot supplied by the embedded app. Revising a saved revision means creating a new one; updating or deleting one is refused with `IMMUTABLE: create a new design revision`.

Two image assets are required per revision.

| Field             | Requirement                                                        |
| ----------------- | ------------------------------------------------------------------ |
| `type`            | Exactly `panel-layout` or `stringing-components`                   |
| Format            | PNG only                                                           |
| Long edge         | At least **2400 px**                                               |
| `revisionId`      | The revision the image belongs to                                  |
| `sha256`          | Lowercase hex checksum of the exact bytes                          |
| `width`, `height` | Must match the uploaded image exactly                              |
| `attribution`     | Non-empty imagery attribution, preserved and shown to the customer |
| `address`         | Must equal the revision's context address                          |
| `framingId`       | Non-empty; **both images in a bundle must share it**               |
| `northIndicator`  | Must be literally `true`                                           |
| `complete`        | Must be literally `true`                                           |

**Upload shape.** Upload uses a signed upload URL with upsert disabled, then a separate completion call. Completion re-reads the stored object and verifies byte length within the 20 MiB bucket limit, checksum, PNG format, and exact width and height. Any mismatch fails with `422 VALIDATION` — `Image bytes, dimensions or checksum do not match.` Completing before uploading fails with `409 ASSETS_NOT_READY` — `Upload the image before completing it.`

**Storage.** Bucket `roof-design-private`; private; 20 MiB file size limit; allowed MIME type `image/png` only. Object paths are generated server-side as `workspace/project/revision/type.png` and are unique per revision and type. There is exactly one row per revision and type: a second upload attempt is refused with `VALIDATION: immutable asset metadata`. Once an asset is `ready` it is immutable — trigger-level guards reject deletion and any change other than status and completion time.

**Bundle completeness.** A bundle exists only when **both** assets for the same revision are `ready`, ordered `panel-layout` then `stringing-components`, with equal revision, equal address to the site address, a long edge of at least 2400, and the same `framingId`. Reading an asset re-verifies that its path belongs to the requested bundle and re-hashes the bytes, failing with `409 Design image integrity check failed.` on mismatch. A bundle cannot be completed from a revision whose customer is unset.

An incomplete attempt never replaces a prior complete bundle. The refusal is `409 ASSETS_NOT_READY` — `Both completed design images are required. The previous bundle is retained.`

### Design artefacts and electrical scope

The integration transmits and stores design artefacts. It does not interpret them.

| Artefact                       | Producer                                                       | ArcSolar's role                                                                                                                         |
| ------------------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Panel layout                   | External editor (`engine: "simple-designer"`)                  | Stores the editor document; stores the rendered `panel-layout` PNG; labels and places it                                                |
| Stringing and component layout | External editor                                                | Stores the rendered `stringing-components` PNG; labels and places it                                                                    |
| Single line diagram (SLD)      | External editor. SLD *state* lives inside the document payload | Persists the payload losslessly and never reads it back. **ArcSolar has no SLD surface.**                                               |
| Measurements and annotations   | External editor                                                | Persisted inside the opaque payload, for example a `measurement` object with `class: "cable"` and points. Never parsed, never surfaced. |

`payload` is `z.record(z.string(), z.unknown())`, annotated in `contract.ts` as "Opaque, lossless editor state: the integration never reconstructs geometry." A payload key may hold any shape. Nothing in ArcSolar enumerates or validates the document's internal keys beyond `version` and `engine`, and unknown keys are retained — which is what makes the persistence lossless.

**No electrical calculation exists anywhere in this integration.** There is no code path that sizes a string, sums a string voltage, checks an MPPT window, computes a cable run or a voltage drop, or validates a design against `max_system_voltage_v` or the inverter MPPT limits. The only electrical data ArcSolar touches is the read-only reference catalogue it serves at `GET /api/roof-design/v1/catalogue` from the shared `solar_catalogue_*` rows: panel `voc_v`, `isc_a`, `impp_a`, `vmpp_v`, `temp_coeff_voc_pct_per_c`, `temp_coeff_isc_pct_per_c`, `max_system_voltage_v` and `max_series_fuse_a`, plus inverter and per-tracker MPPT rows (`mppt_voltage_min_v`, `mppt_voltage_max_v`, `max_input_current_per_mppt_max_a`, `max_isc_per_mppt_max_a`, `strings_per_mppt_max`, and per-tracker `strings`). Those rows are shared reference data carrying no `workspace_id`, returned as stored, and every measurement is text that the editor normalises. **Serving a specification is not evaluating it.**

An integrator should therefore treat the document as write-only from ArcSolar's perspective: send it, have it stored, get it back unaltered. Do not expect ArcSolar to reject an electrically invalid design, and do not ask ArcSolar for a derived electrical figure. If a stringing or SLD value is not inside the payload or drawn on an image, ArcSolar does not have it.

### Error codes

| Code               | Status | Meaning                                                  |
| ------------------ | ------ | -------------------------------------------------------- |
| `UNAUTHENTICATED`  | 401    | No valid session or capability                           |
| `LAUNCH_EXPIRED`   | 401    | Launch code reused or past its 60-second life            |
| `FORBIDDEN`        | 403    | Wrong origin, wrong project scope, or missing capability |
| `NOT_FOUND`        | 404    | Missing project, revision, asset, image, or draft        |
| `DISABLED`         | 404    | Host origin or launch encryption is not configured       |
| `VERSION_CONFLICT` | 409    | `expectedVersion` did not match the stored version       |
| `ASSETS_NOT_READY` | 409    | A required image is missing, incomplete, or inconsistent |
| `REVIEW_REQUIRED`  | 409    | The design does not match the saved commercial draft     |
| `VALIDATION`       | 422    | Invalid payload, document, or image bytes                |
| `UNAVAILABLE`      | 503    | Temporary failure or an unresolvable draft               |

Underlying database errors are mapped deterministically: `42501` becomes `403 You do not have access to this design.`, `40001` or `PT409` become `409 VERSION_CONFLICT The design changed. Reload before saving.`, and anything else becomes `422 VALIDATION`, with a message already prefixed `VALIDATION:` passed through verbatim.

### Permissions

| Surface                     | Who                                                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Read a design               | Active workspace member with access to the linked customer, plus an explicit grant for address-only projects |
| Mutate a design             | Roles `administrator`, `developer`, `operations`, `sales`, with an active auth session                       |
| Grant or share              | Workspace owner, `administrator`, or `developer`                                                             |
| Organisation-level defaults | `administrator` or `developer`                                                                               |

Row-level security is enabled on every table. Browser roles are granted `select` only, except the launch-exchange table, which has no browser grant and no policy at all — its contents are server-only. Command execution requires a current auth session and one of the mutating roles.

### Availability

Roof Design is live in the standard product. The gates are enforced per request, not at configuration time.

| Gate                      | Applies to                                                       | Failure                                                 |
| ------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------- |
| `quote.revise` capability | Cookie-authorised writes — `launch`, `attach`, and cookie writes | `403 You do not have permission to edit quote designs.` |
| Origin validation         | Every cookie-authorised write, and CORS                          | `403 Request origin denied.`                            |

There is **no entitlement gate on this path**. A `roof_vision` entitlement exists but is applied to the older analysis paths, not to this route or this panel. Reads require active workspace membership plus access to the linked customer, with an explicit grant for address-only projects.

The permitted embedded host resolves from `ROOF_DESIGN_ORIGIN`, which defaults to `https://design.arcsolar.com.au`. The value must be a bare https origin with no path; `localhost` and `127.0.0.1` are accepted for local development, and any other host is rejected at resolution time.

### Configuration accuracy

Four `ROOF_DESIGN_QA_*` names — `ROOF_DESIGN_QA_ENABLED`, `NEXT_PUBLIC_ROOF_DESIGN_QA_ENABLED`, `ROOF_DESIGN_QA_ORIGIN` and `ROOF_DESIGN_QA_PROJECT_REF` — appear in `.env.example` and earlier contract text but are read by **no application code**. Runtime configuration is `ROOF_DESIGN_ORIGIN` (default `https://design.arcsolar.com.au`) and `ROOF_DESIGN_EXCHANGE_KEY`, the latter falling back to a key derived from `SUPABASE_SECRET_KEY` / `SUPABASE_SERVICE_ROLE_KEY`. Do not rely on the `ROOF_DESIGN_QA_*` names for gating: setting them has no effect, and omitting them does not disable the feature.

### Data shapes

The integration keeps nine tables, all workspace-scoped and all registered in the tenant purge manifest.

| Table                          | Holds                                                                                                                                                                                        |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `roof_design_projects`         | One design project: address, coordinates, optional customer, site and opportunity, the design document, selected equipment, and a positive integer `version` used for optimistic concurrency |
| `roof_design_grants`           | Per-member edit grants for address-only projects                                                                                                                                             |
| `roof_design_revisions`        | Immutable checkpoints with document, context snapshot and equipment snapshot                                                                                                                 |
| `roof_design_assets`           | One row per revision and image type, with checksum, dimensions, metadata and `pending` or `ready` status                                                                                     |
| `roof_design_quote_links`      | The reviewed attachment: revision, quote draft, bundle, commercial fingerprint, reviewer, and the issued estimate if any                                                                     |
| `roof_design_equipment`        | The equipment catalogue and its mapping to commercial products                                                                                                                               |
| `roof_design_preferences`      | Organisation- or member-scoped defaults                                                                                                                                                      |
| `roof_design_events`           | Append-only audit events                                                                                                                                                                     |
| `roof_design_launch_exchanges` | Server-only launch codes and session capabilities                                                                                                                                            |

Key interfaces available to the embedded app include a command entry point (`roof_design_command`), a context reader (`roof_design_context`), a defaults writer (`roof_design_defaults`), and a member-scoped quote draft reader (`get_quote_builder_draft`). The draft reader returns only the caller's own draft, and settlement requires the returned id to equal the id the caller named.

**Context shape returned to the editor.** Version 1, with the organisation id, read/edit/attach permissions, optional customer, site and opportunity ids, address, coordinates, optional NMI evidence (value, evidence id, site id), and up to 200 selected equipment lines. Note that the draft id in this context is not the live quote draft id; the live id comes from the launch exchange.

### Two design systems can appear in one estimate

An estimate snapshot can carry a `roofDesign` bundle or a legacy `solarDesign` payload. They are different features with different producers, and they must never be described as one.

|                  | Roof Design bundle                                                                                                                                                                                                                                                                                                                                                                                                                        | Legacy provider solar design                                                                                                                                 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Snapshot key     | `roofDesign` (`proposalDesignBundleSchema`)                                                                                                                                                                                                                                                                                                                                                                                               | `solarDesign` (`solarRoofDesignSchema`)                                                                                                                      |
| Provider literal | `z.literal("roof-design")`                                                                                                                                                                                                                                                                                                                                                                                                                | `z.enum(["google_solar", "manual", "prototype"])`                                                                                                            |
| Producer         | The external Roof Design editor, over the iframe handshake                                                                                                                                                                                                                                                                                                                                                                                | Google Solar API building insights, or a restored prototype layout                                                                                           |
| Holds            | Two PNG assets plus the selected equipment lines                                                                                                                                                                                                                                                                                                                                                                                          | Modelled roof segments, per-panel geometry and energy model, imagery quality, `detectedArrayStatus`                                                          |
| Customer heading | Per surface: estimate portal section nav **Roof design**; estimate PDF section heading **PROPOSED ROOFTOP DESIGN**, captioned **PROPOSED PANEL LAYOUT** and **PANEL STRINGS & COMPONENT MARKINGS**; contract and tenant-document PDFs **Solar panel layout** and **Stringing and component layout**. The heading **PROPOSED ROOFTOP DESIGN** is shared with the legacy branch, so it does not by itself indicate which system is showing. | **Proposed rooftop design**, with a provider badge from `getSolarDesignProviderLabel`                                                                        |
| Metrics          | None. No capacity, energy or exposure figure is produced from this bundle                                                                                                                                                                                                                                                                                                                                                                 | `getSolarDesignSummary` yields Designed capacity, Panel layout, Annual DC energy and Average exposure                                                        |
| Entitlement      | None beyond `quote.revise` and origin validation                                                                                                                                                                                                                                                                                                                                                                                          | `roof_vision`, via `requireArcFeature`, reserved for paid Arc accounts                                                                                       |
| Modules          | `src/lib/roof-design/*`                                                                                                                                                                                                                                                                                                                                                                                                                   | `src/lib/quotes/solar-design.ts`, `solar-layout.ts`, `google-solar-analysis.server.ts`, `solar-proposal-image.server.ts`, `solar-customer-preview.server.ts` |

**How they relate.** ArcSolar is moving from the provider-based path to the editor, and both paths are live code today.

* The builder presents the Roof Design panel. Accepting a design calls `updateSolar({ roofDesign, design: null })`, which clears the legacy design.
* Issue-time validation branches on `!draft.solar.roofDesign`. When a roof design is attached, only the Roof Design checks run. When it is not, the legacy checks run, including the 30-day Google Solar freshness rule and `requireArcFeature(..., "roof_vision")`.
* So the `roof_vision` entitlement gates the **legacy** path at issue time, never the editor.
* Both estimate branches share the DOM id `portal-roof`, which is safe only because the builder guarantees one is null.
* PDF behaviour is per document, and both statements are true of different files. The **estimate PDF** (`src/lib/quotes/estimate-pdf.ts`) **supersedes**: `solarDesignContent` early-returns `roofDesignContent` when a bundle is present, so the legacy `solarDesign` branch is unreachable while one exists. The **contract PDF** (`src/lib/quotes/contract-pdf.ts`) and the **tenant-document PDF** (`src/lib/quotes/tenant-document-pdf.ts`) **append** one A4 landscape page per image through `appendRoofDesignPages`, which the estimate PDF never calls.

**Keep them separate when answering.** A question about capacity, annual energy, exposure, `detectedArrayStatus` or a provider badge belongs to the legacy path. A question about the two layout images, revisions, framing, or the attach and review flow belongs to the Roof Design editor. Do not attribute a Google Solar figure to the Roof Design editor, and do not describe the editor as producing the legacy metrics.

### Genuinely undetermined

Do not present any of the following as settled behaviour.

* **Search result shape.** The type union includes an external-address result, but the search route only ever builds customer and existing-design results. Where external address results come from is not settled here.
* **Equipment has no write path.** The equipment table is readable but no command action creates or maps equipment rows. How the catalogue is populated is unresolved.
* **"Reviewed" is not persisted as a state.** The review flag is validated on the attach request but never stored; the only durable record of review is the quote-link row. Any state machine described for a design must be expressed in terms of the project version, the asset status, and the presence of a link row.
* **The legacy editor path.** The contract states that `/designer` remains the legacy editor while `/` is the shell, but nothing in the application defines or verifies `/designer`.
* **SLD and stringing semantics.** The payload carries SLD state, measurements and cable-class discriminators, but its shape inside the payload is typed `unknown` and no ArcSolar code reads it. ArcSolar's own verifiable description of the stringing image is limited to its label and the portal alt text `Roof imagery showing string assignments, connection order and components`. Anything beyond that — string architecture, per-string module counts, string voltages, MPPT allocation — cannot be confirmed from this repository.
* **Whether the legacy provider path is being retired.** Both paths are live code and both are validated at issue time, with no dated removal and no deprecation flag. Whether Google Solar analysis is intended to remain available alongside the editor is not settled by the code.

## Related

* [Use Roof Design (Roof Vision)](/quoting/roof-design) — the working guide.
* [Feature availability](/reference/availability) — how gated features are presented.
* [Get help from ArcSolar](/reference/support) — how to get help.


## Related topics

- [Use Roof Design](/quoting/roof-design.md)
- [Design the roof and the panels](/roof-design/editor.md)
- [Draw the single line diagram](/roof-design/sld.md)
- [What's new in ArcSolar](/reference/whats-new.md)
- [Coordinate field operations and installers](/delivery/field-operations.md)
