Skip to content

jusso-dev/ScopeStack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

83 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ScopeStack

ScopeStack

A branded Statement of Work, proposal, and quote workflow for consultants, MSPs, agencies, and professional services teams.

Build approved internal templates once, then deliver consistent client-facing documents with a clear path through review, revision, and acceptance β€” every step audited.

This is an MVP scaffold, end-to-end functional, suitable as a launching pad for a real product.

Product screenshots

The screenshots below are generated from the end-to-end Playwright workflow in scripts/capture-workflow-screenshots.ts. Each checkpoint includes light and dark mode captures.

1. Create a workspace
Light Dark
ScopeStack signup form in light mode ScopeStack signup form in dark mode
2. Workspace dashboard
Light Dark
ScopeStack analytics dashboard in light mode ScopeStack analytics dashboard in dark mode
3. Published SOW template
Light Dark
Published SOW template list in light mode Published SOW template list in dark mode
4. Add the primary client
Light Dark
Filled primary client form in light mode Filled primary client form in dark mode
5. Primary client created
Light Dark
Primary client created in light mode Primary client created in dark mode
6. Add a second client
Light Dark
Filled secondary client form in light mode Filled secondary client form in dark mode
7. Clients list
Light Dark
Clients list with two clients in light mode Clients list with two clients in dark mode
8. New document from the SOW template
Light Dark
New SOW document form in light mode New SOW document form in dark mode
9. Edit document and pricing
Light Dark
Document editor with pricing line items in light mode Document editor with pricing line items in dark mode
10. Document ready to send
Light Dark
Document detail page ready to send in light mode Document detail page ready to send in dark mode
11. Send document email
Light Dark
Send document form filled in light mode Send document form filled in dark mode
12. Secure client portal link generated
Light Dark
Document sent with secure portal link in light mode Document sent with secure portal link in dark mode
13. Client portal review
Light Dark
Client portal document review in light mode Client portal document review in dark mode
14. Client leaves a note
Light Dark
Client comment form filled in light mode Client comment form filled in dark mode
15. Client note posted
Light Dark
Client comment posted in portal activity in light mode Client comment posted in portal activity in dark mode
16. Client acceptance form
Light Dark
Client acceptance form filled in light mode Client acceptance form filled in dark mode
17. Client approved and locked
Light Dark
Client portal after document approval in light mode Client portal after document approval in dark mode
18. Internal accepted document record
Light Dark
Internal accepted document detail in light mode Internal accepted document detail in dark mode

What's inside

  • Next.js 16 (App Router) + React 19.2 + TypeScript + Tailwind + shadcn-style UI
  • Prisma + PostgreSQL (local docker-compose, ready for managed Postgres in prod)
  • TipTap rich text editor in template & document section builders
  • Resend + SMTP (nodemailer) for emails, with a unified EmailLog
  • Trigger.dev for scheduled and background jobs
  • Playwright to render branded PDFs from the same HTML the client sees
  • Better Auth for email/password auth with DB-backed sessions (scrypt hashing)
  • Zod validation at every boundary, isomorphic-dompurify for rich text
  • Vitest test suite covering pricing, templates, documents, and isolation

Quickstart

Requires Node.js 20.9+ for Next.js 16.

# 1. Install JS deps
npm install
# Playwright tries to download Chromium. If your network blocks that, run:
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install
# You can then install the browser later with:
#   npx playwright install chromium

# 2. Start Postgres (creates `scopestack` and `scopestack_test` databases)
docker compose up -d

# 3. Configure environment
cp .env.example .env
# Defaults point at the docker-compose Postgres. Set BETTER_AUTH_SECRET
# (32+ chars), BETTER_AUTH_URL, and wire up RESEND_API_KEY if you want
# real outbound email.

# 4. Push the schema
npx prisma db push

# 5. Seed the example IT/MSP organisation
npm run seed

# 6. Run the app
npm run dev
# β†’ http://localhost:3000

Background jobs are handled through Trigger.dev. The app works without running Trigger locally, but automated reminder jobs will only execute when the Trigger dev CLI or a deployed Trigger worker is running. See Background jobs: Trigger.dev.

Next.js 16 uses Turbopack by default for next dev and next build. Linting is run separately through ESLint:

npm run lint

Using your own Postgres

If you already have a Postgres instance, skip docker compose up and point DATABASE_URL (and TEST_DATABASE_URL if you want to run tests) at it:

DATABASE_URL="postgresql://user:pass@host:5432/scopestack?schema=public"
TEST_DATABASE_URL="postgresql://user:pass@host:5432/scopestack_test?schema=public"

Seeded credentials

email:    owner@scopestack.local
password: password123

The seed creates Northbeam IT Services β€” a managed IT services consultancy β€” with branding, a primary client (Harbourline Retail), one published SOW template, a draft template, and three documents (draft / sent / accepted).

Workflow

  1. Sign in β€” owners and admins manage branding and templates.
  2. Settings β†’ Branding β€” set logo URL, colours, address, currency, tax label, payment terms, footer, disclaimer.
  3. Settings β†’ Email β€” choose platform sending (Resend) or your own SMTP server. All emails land in the email log regardless.
  4. Templates β†’ New template β€” start from the default SOW outline; edit sections with TipTap; use {{client.company}} style variables for personalisation. Save draft, then publish to lock a version.
  5. Documents β†’ New document β€” pick a published template + client; the document snapshots the template content so future template edits never affect existing documents.
  6. Document detail β€” edit sections, add line items (quantity, unit price, tax, discount, billing frequency), pick a primary contact, and send to the client.
  7. Send β€” generates a secure token, logs the email, and flips the document to sent.
  8. Client portal (/client/document/[token]) β€” branded read-only view with options to accept, request changes, comment, or decline. Rate limited.
  9. Revision requests flip the document to revision_requested and notify the internal owner via email. Marking revised creates a new DocumentVersion and bumps the revision number.
  10. Acceptance captures name, email, job title, typed signature, IP, user agent, and the accepted revision number. It locks the document (lockedAt), generates the final PDF with an acceptance certificate page, and emails both parties.

Roles

owner, admin, member, viewer. The signup flow creates an owner. Invitations and per-page role checks are easy to extend on top of the existing OrganisationMember.role column.

Project layout

prisma/
  schema.prisma         All models, with organisation isolation everywhere
  seed.ts               Northbeam IT Services example

src/
  app/
    (app)/              Authed area (dashboard, templates, documents, clients,
                        settings) β€” shares the AppShell layout.
    client/document/    Public client portal.
    api/                Route handlers β€” Better Auth catch-all at
                        /api/auth/[...all]; templates, documents, settings,
                        and client portal actions.
  trigger/              Trigger.dev scheduled/background task entrypoints
  components/
    ui/                 Buttons, inputs, cards, badges, selects (shadcn style)
    editor/             TipTap rich text editor + template/document editors
    document/           SendForm
    client/             Portal actions (accept/revise/decline/comment)
    shell/              AppShell + PageHeader
    brand/              Stack-logo mark + wordmark
  lib/
    prisma.ts           Prisma singleton
    better-auth.ts      Better Auth server config (Prisma adapter, email+password)
    auth-client.ts      Better Auth React client (createAuthClient)
    auth-actions.ts     loginAction / signupAction / logoutAction server actions
    auth.ts             requireUser / requireMembership / requireManager
    tokens.ts           Secure token + reference generation
    enums.ts            Role / status / type unions (SQLite has no enums)
    validation.ts       Zod schemas for every input
    pricing.ts          Subtotal / discount / tax / grand total math
    sanitize.ts         DOMPurify-backed HTML sanitiser
    variables.ts        {{variable.path}} substitution
    default-sections.ts 16-section SOW outline
    documents.ts        Load Document + branding for render
    document-actions.ts Create / update / revise / token helpers
    document-reminders.ts Due reminder selection and reminder email dispatch
    document-tokens.ts  Portal token issue/revoke helpers safe for jobs
    templates.ts        Create / update / publish (versioning logic lives here)
    render-document.ts  Branded HTML the client portal + PDF both use
    pdf.ts              Playwright bridge (graceful fallback if missing)
    storage.ts          FileStorage interface + LocalStorage (S3/R2 ready)
    email.ts            Resend + SMTP dispatch with EmailLog
    audit.ts            recordAudit helper
    rate-limit.ts       Token-bucket limiter for public endpoints
    api.ts              Standard NextResponse helpers

tests/
  pricing.test.ts       Subtotal/discount/tax math
  templates.test.ts     Publish creates version; editing a published template
                        creates a new draft without mutating the live one
  documents.test.ts     Snapshotting, sending, revision, acceptance lock,
                        organisation isolation

Security highlights

  • Organisation isolation β€” every query filters by organisationId.
  • Role-based permissions β€” manager-only writes for branding, email config, Slack config, org rename. Easy to extend per-resource.
  • Client portal tokens hashed and encrypted at rest β€” 32 random bytes, base64url, time-limited. The database stores a SHA-256 hash for indexed lookups plus an AES-256-GCM ciphertext for idempotent re-issue. A DB-only leak does not yield usable portal URLs.
  • AES-256-GCM at rest for sensitive secrets β€” SMTP passwords and Slack webhook URLs are encrypted with a key derived from APP_ENCRYPTION_KEY via HKDF-SHA256 (falls back to legacy TOKEN_ENCRYPTION_KEY, then BETTER_AUTH_SECRET).
  • CSRF defense on the public portal β€” every mutation endpoint requires the x-scopestack-portal header, rejects cross-site sec-fetch-* loads, and enforces Content-Type: application/json.
  • Strict security headers β€” global CSP (no eval, no remote scripts), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin (no-referrer on the portal), Permissions-Policy lockdown, Strict-Transport-Security, COOP/CORP same-origin, Cache-Control: no-store on portal pages and all client API responses.
  • Rate limiting β€” in-memory token bucket on every public token endpoint (view, revision, comment, decline, accept, pdf). clientIp() ignores forwarded headers unless TRUST_PROXY=true so an attacker cannot spoof per-IP limits. Replace with Redis for multi-process deploys.
  • Bounded JSON bodies on all portal endpoints (64 KB) to mitigate memory DoS.
  • Zod validation at every API boundary; Slack webhook URLs are validated against https://hooks.slack.com/services/ only to prevent SSRF abuse of stored credentials.
  • HTML sanitisation before storage and again on render via isomorphic-dompurify; client-supplied free text additionally has ASCII control characters stripped before storage / Slack / email.
  • Generic 500 responses β€” internal error details are logged server-side only; clients receive a generic message so paths, libraries, and query shapes cannot leak.
  • Lock on acceptance β€” lockedAt is set when a client accepts; the updateDocument helper rejects writes to locked documents.
  • No client access to internal notes β€” internalNotes and internal comments are never sent to the client portal.
  • Audit log for every meaningful event (template lifecycle, document lifecycle, email send/fail, view, PDF download, accept, decline, revise, comment, Slack config change).

Slack notifications

Configure under Settings β†’ Notifications. Provide a Slack incoming webhook URL (https://hooks.slack.com/services/…) and pick which events should post to the channel:

  • Client opens a proposal β€” fires once on first view.
  • Client posts a comment
  • Client requests a revision
  • Client declines
  • Client accepts

The webhook URL is encrypted at rest, validated against Slack's host (no arbitrary outbound HTTP), redacts ASCII control characters and Slack mrkdwn metacharacters from user-supplied content, and is delivered fire-and-forget with a 5-second timeout so a Slack outage cannot block the portal.

Background jobs: Trigger.dev

Trigger.dev is the chosen path for background jobs in this project. Use it for scheduled work, retries, long-running tasks, and future async workflows instead of adding ad hoc cron scripts or in-process timers to the Next.js app.

The first Trigger task is document-reminders, defined in src/trigger/document-reminders.ts. It runs on this declarative schedule:

cron: { pattern: "0 9 * * *", timezone: "Australia/Sydney" }

The task calls sendDueDocumentReminders() in src/lib/document-reminders.ts. That function:

  • finds sent/viewed document recipients that are due for a reminder
  • respects each document's reminder settings (remindersEnabled, reminderAfterDays, reminderIntervalDays)
  • creates or reuses a secure recipient portal token
  • sends a branded reminder email through the same sendEmail() path as the app
  • records EmailLog, AuditLog, lastReminderAt, and reminderCount

Local Trigger setup

Install dependencies first:

npm install

Create .env from the example file and add your Trigger project id. .env is ignored by git and must never be committed.

cp .env.example .env

Required local value:

TRIGGER_PROJECT_ID="proj_..."

Start the Next.js app and Trigger dev worker in separate terminals:

npm run dev
TRIGGER_PROJECT_ID="proj_..." npx trigger.dev@latest dev

In dev mode, Trigger loads the task in the Trigger.dev dashboard, but the code runs on your machine. That means it can use your local Postgres, local .env, and local Resend configuration. This is the right mode when you do not yet have a hosted app and hosted database.

Viewing and testing schedules

After the Trigger dev CLI starts successfully, open Trigger.dev and go to:

Project β†’ Tasks β†’ document-reminders

From there you can inspect the schedule, manually test the task, and see run logs. The schedule will only run automatically while the dev CLI is active in development. In staging/production it runs from the latest deployed Trigger worker.

Deployment

Do not commit Trigger project ids or secrets to source control. For local deploys, pass the project id through the shell environment:

TRIGGER_PROJECT_ID="proj_..." npx trigger.dev@latest deploy

For CI/CD, store TRIGGER_PROJECT_ID as a CI secret and run the same deploy command from the pipeline.

Trigger cloud workers run on Trigger.dev infrastructure, not your app server. They need their own environment variables configured in the Trigger.dev dashboard for the target environment. Add the same server-side values your deployed app uses:

DATABASE_URL="postgresql://..."
APP_URL="https://your-app.example.com"
NEXT_PUBLIC_APP_URL="https://your-app.example.com"
APP_ENCRYPTION_KEY="same value as the app"
BETTER_AUTH_SECRET="same value as the app"
RESEND_API_KEY="..."
RESEND_FROM_EMAIL="ScopeStack <no-reply@yourdomain.com>"

The database must be reachable from Trigger.dev. A localhost database on your laptop will not work for deployed Trigger runs. APP_URL must also be a real public URL so reminder emails contain usable client portal links.

Keep APP_ENCRYPTION_KEY aligned with the deployed app. Reminder jobs decrypt existing portal token ciphertext when reusing secure recipient links, so a different encryption key will break link generation.

Product reminder controls

Each document has a Reminders panel on the document detail page. Users can:

  • enable or pause automatic reminders for that document
  • set the first reminder delay in days
  • set the repeat cadence in days
  • see per-recipient reminder counts and next due timing
  • review recent reminder activity from audit events

Accepted/locked documents stop sending reminders and their reminder settings become read-only.

Integrations: Monday.com and Xero

Configure under Settings β†’ Integrations.

Monday.com

  • Pull contacts: items from a chosen board become ScopeStack clients / contacts. Items without an email column are skipped (ScopeStack contacts require an email). Each Monday item id is recorded in ContactExternalId so subsequent pulls update the same row instead of creating duplicates.
  • Push contacts: from the clients UI, a single ScopeStack contact can be mirrored to the Monday board β€” first push creates the item; later pushes update column values in place.
  • Column mapping is per-organisation: provide the Monday column ids for email / phone / job title / company. Unmapped fields are silently skipped.
  • The Monday API token is stored AES-256-GCM encrypted at rest. All requests hit the Monday GraphQL endpoint (https://api.monday.com/v2) with a 15-second timeout and reject redirects (so SSRF cannot be coaxed out of a future URL-templating bug).

Xero

  • OAuth2 (authorisation code with confidential client). Register a Web App at developer.xero.com with redirect <APP_URL>/api/integrations/xero/callback, then set XERO_CLIENT_ID and XERO_CLIENT_SECRET in the environment.
  • The connect flow binds an HTTP-only xero_oauth_state cookie to a packed state containing the organisation id, so callbacks cannot be forged across organisations.
  • Access + refresh tokens are stored AES-256-GCM encrypted; access tokens auto-refresh within 60s of expiry, and the rotated refresh token is persisted on every refresh.
  • Invoice on acceptance: when a proposal / SOW is accepted in the client portal, ScopeStack ensures a Xero contact exists for the primary contact (looked up by email first; created otherwise), then creates an ACCREC invoice mirroring the proposal's line items, currency, payment terms (Net N is parsed for the due date) and reference. The freshly-generated proposal PDF is uploaded as a Xero invoice attachment.
  • Idempotent: ExternalInvoice records the (document, provider) pair β€” retries return the existing invoice rather than double-charging. Failures are recorded with status=FAILED and surfaced on the Integrations page with a retry button.
  • Draft by default: invoice status is DRAFT so a human reviews before Xero emails the customer. Switch to AUTHORISED if you want the system to finalise invoices automatically.

Tests

npm test

Vitest points Prisma at the scopestack_test Postgres database (override with TEST_DATABASE_URL), force-resets the schema before the suite runs, and covers:

  • Template publishing creates a version.
  • Editing a published template creates a new draft (live version preserved).
  • Document creation snapshots template content (later template edits do not reach existing documents).
  • Send flow creates a token + logs an email.
  • Revision flips status, then reviseDocument bumps revision number and creates a new DocumentVersion.
  • Acceptance locks the document β€” later edits throw.
  • Pricing totals (subtotal, discount, tax, grand total).
  • Organisation isolation β€” one workspace cannot read or write another's docs.

Email modes

  • Platform (default): set RESEND_API_KEY in .env. Otherwise outbound email is captured in EmailLog with status queued so the UI still flows end-to-end without a Resend account.
  • Custom SMTP: in Settings β†’ Email switch to custom and provide host/port/user/password/secure/from. Sent via nodemailer.

PDF generation

Renders the same HTML the client sees, then captures it with Playwright Chromium and stores it through the FileStorage abstraction (LocalStorage implementation lives in src/lib/storage.ts). If Chromium isn't installed, PDF endpoints gracefully fall back to serving the printable HTML so the UX still works.

To install browsers:

npx playwright install chromium

Going to production

  • Point DATABASE_URL at a managed Postgres (RDS, Supabase, Neon, etc.) and apply migrations with npx prisma migrate deploy.
  • Set a strong BETTER_AUTH_SECRET (32+ chars) and BETTER_AUTH_URL to your public URL.
  • Set APP_URL and NEXT_PUBLIC_APP_URL to your public URL β€” used in client portal links and the Better Auth React client.
  • Configure RESEND_API_KEY and RESEND_FROM_EMAIL.
  • Swap LocalStorage for an S3/R2 implementation against the same FileStorage interface.
  • Replace the in-memory rateLimit with Redis/Upstash for multi-process deployments.
  • Add user invitations / role assignment UI on top of the existing OrganisationMember.role field.
  • Encrypt EmailConfiguration.smtpPassword at rest.

Brand notes

ScopeStack's visual identity is clean, professional, modern, and trustworthy. The wordmark uses a stacked layer logomark with a subtle checkmark to suggest scope, structure, and approval.

--scope-navy:  #071B5F
--scope-blue:  #1479FF
--scope-sky:   #58B7FF

The palette is mirrored in Tailwind (bg-scope-navy, text-scope-blue, …) and embedded in the document renderer's CSS variables so client-facing documents and PDFs stay on-brand.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages