A-Grade Docs
Developer

Sales Documents App

Current behavior, architecture, authentication, deployment, and document-handling conventions for the A Grade sales workspace.

Sales Documents App

The sales app is an internal A Grade Contracting workspace for creating, editing, printing, and managing proposals and invoices. It lives in apps/sales and is intended to deploy independently at sales.agradecontracting.com from the same Bun monorepo as the public website and documentation.

The app is functional today with Supabase authentication and a shared, versioned document store. Client instructions live in the Sales App Guide. The Document Store Plan remains the engineering record for the persistence design.

Current Stack

ConcernImplementation
ApplicationVite, React, and TypeScript in apps/sales
Package managementBun workspace from the repository root
Shared contractspackages/shared/src/documents.ts
AuthenticationSelf-hosted Supabase Auth using the public browser key
Current persistenceShared Supabase document store through the Sales API
Document outputResponsive editor plus print-ready invoice/proposal preview
Deployment targetCoolify/Nixpacks static build at sales.agradecontracting.com
APIBun and Hono at api.agradecontracting.com

Document Capabilities

The current app supports:

  • Invoice and proposal templates.
  • Editable sender, customer, project, date, terms, notes, and schedule fields.
  • Line items with quantity and unit pricing.
  • Optional add-ons.
  • Proposal acceptance fields.
  • Print-ready document layouts.
  • Duplication and non-destructive archiving of shared documents.
  • Database-assigned invoice and proposal numbers.
  • Optimistic version checks that prevent stale saves from replacing newer work.

JSON import/export is available only in the explicitly selected browser-local development track. It is not rendered and cannot read or write documents while the app is on its production remote track.

Authentication And Access

The app uses a persistent Supabase browser session and supports:

  • Password sign-in.
  • Password recovery.
  • Recovery-session password updates.
  • Sign-out.

Public registration is intentionally absent. Access is currently constrained in packages/shared/src/auth.ts to:

The browser allowlist improves product behavior but is not the authorization boundary. Public signup remains disabled in Supabase, and the document store authorizes users by their Supabase user IDs through platform.project_memberships and database policies.

The document-store migration can add a membership only when the matching account already exists in auth.users. After creating an approved Sales account, run apps/api/supabase/operations/provision_agrade_sales_users.sql in the Supabase SQL editor. The operation is idempotent and reports whether each approved account is missing or provisioned.

Auth Redirects

The password-recovery request uses window.location.origin, so the requested return destination is selected at runtime:

Local development: http://localhost:5173
Production:        https://sales.agradecontracting.com

Both origins must be present in the self-hosted Supabase Auth redirect allowlist. SITE_URL remains the fallback destination, while ADDITIONAL_REDIRECT_URLS contains every application origin or callback path that GoTrue may honor.

Example shared-instance configuration:

SITE_URL=https://app.iiicoast.tech
ADDITIONAL_REDIRECT_URLS=https://app.iiicoast.tech,http://localhost:5173,https://sales.agradecontracting.com

After changing the self-hosted Supabase environment, recreate the Auth container and request a new recovery email:

docker compose up -d --no-deps --force-recreate auth

Existing email links keep the destination embedded when they were generated.

Persistence Tracks

The production app runs on the remote persistence track. It starts with an empty loading state, loads the user's project through the Sales API, and displays only documents returned by the shared store. A remote failure produces a retry screen and never exposes stale browser-local examples.

Browser-local persistence is retained only as an explicitly selected development track. It requires VITE_DOCUMENT_STORE_MODE=local at build time and a reload. The app does not switch tracks while it is running, and failed remote writes never fall through into local storage.

An explicitly enabled one-time migration path can read existing browser drafts and submit them through the normal create API. It remains off by default.

Shared Document Model

AGradeDocument is defined in packages/shared/src/documents.ts. It currently includes:

  • Document identity, type, number, and date.
  • Sender and customer parties.
  • Project and section labels.
  • Line items and optional add-ons.
  • Notes, schedule, acceptance, and footer configuration.

The shared package is the contract boundary. Database rows, API request/response types, runtime validation, and the sales UI should evolve together rather than duplicating document shapes inside individual apps.

Local Development

From the repository root:

bun install
bun dev

Portless starts the complete workspace at:

Sales: https://sales.agrade
API:   https://api.agrade
Web:   https://web.agrade
Docs:  https://docs.agrade

Use bun run dev:plain when Portless is unavailable. The direct Sales and Docs ports are 5173 and 3001 respectively.

Required local browser variables:

VITE_SUPABASE_URL=https://your-self-hosted-supabase.example.com
VITE_SUPABASE_ANON_KEY=your-public-key
VITE_DOCUMENT_STORE_MODE=remote

Local orchestration supplies or derives the API origin. Never expose the Supabase secret/service-role key through a VITE_* variable.

Deployment

Deploy the monorepo root as a separate Coolify application for the sales host:

Install command: bun install --frozen-lockfile
Build command:   bun run sales:build
Publish path:    /apps/sales/dist

Set VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, and VITE_DOCUMENT_STORE_MODE=remote as build-time environment variables. Production defaults to https://api.agradecontracting.com; set VITE_DOCUMENT_API_URL only when intentionally overriding that origin. The static server must fall back to index.html for unknown SPA routes.