Scope of work exhibits, generated properly.

Pick a CSI division, choose from a curated clause library, edit anything you need to, and export a paginated PDF, an editable Word file, Markdown or JSON — all carrying identical clause numbering.

Flask 3 · Python 3.11+ 236 clauses · 20+ trades PDF · DOCX · MD · JSON Self-hosted · MIT

Scope gaps are expensive

Your fire protection subcontractor is contractually responsible for firestopping its own penetrations. That specification section lives in Division 07. If the exhibit only lists Division 21 sections, nobody priced it — and somebody pays for it later.

🧭

Coverage analysis

A matrix across every package on the project: which specification sections nobody has claimed, which two trades are both being paid for, and which are shared by design — every trade firestops its own penetrations, but somebody still has to decide who paints the exposed sprinkler pipe.

🔗

Cross-division references

A Division 21 package is automatically offered the Division 07 firestopping, Division 08 access doors, Division 28 fire alarm interface and Division 33 water main it actually carries.

📚

Language you can standardise

Universal obligations plus trade-specific inclusions, exclusions and clarifications. Copy a shipped clause to make it yours, hide the ones you disagree with, add your own.

🔢

Numbering that means something

Clause 3.2.4 points at exactly one sentence — in the PDF, the Word file, the Markdown and the JSON. Labels are computed once and written literally into every format.

How it works

One numbered document tree, five ways out.

Division 21 (Fire Suppression)
      │
      ├── universal clauses ────────┐
      ├── Division 21 clauses ──────┤
      ├── Division 01 spec sections ┤──▶ ScopeDocument ──▶ outline numbering ──┐
      └── cross-referenced sections ┘         │                                │
           (07 firestopping,                  │                                │
            08 access doors,             editable items                        │
            28 fire alarm…)              in the browser                        │
                                                                               ▼
                                        PDF · DOCX · HTML · Markdown · JSON

Pick the package

Project, bid package and CSI division. Selecting a package fills in its division and trade.

Choose the language

The library pre-selects the standard clauses and specification sections for that division.

Edit anything

Reword, reorder, nest, add your own. The live preview is the document you will export.

Export or issue

Download in any format. Issuing freezes an immutable revision of exactly what went out.

What comes out

The structure the industry actually writes, with the specification sections nested where they belong.

EXHIBIT B – SCOPE OF WORK
Division 21 – Fire Protection

1. Intent

In general, this Bid Package is comprehensive to the specific CSI Division Work and related Work as referenced, indicated on, or implied by the Project Drawings, Specifications and Project Manual…

2. Scope of Work Summary

  1. 1.The Scope of Work shall include the furnishing and installation, complete, of the FIRE PROTECTION Work in accordance with the Contract Documents.
  2. 2.This shall include all required engineering, submittals, materials, fabrication, labor, equipment, supervision, hoisting, licenses, burden, taxes and insurances…
  3. 3.The Scope of Work shall include all Division 01 General Requirements and the Work in the following Specification Section(s):
  1. 3.1211313 – Wet-Pipe Sprinkler Systems
  2. 3.2213113 – Electric-Drive Centrifugal Fire Pumps
  3. 3.3078413 – Penetration Firestopping ← Division 07
  4. 3.4083113 – Access Doors and Frames ← Division 08
  5. 3.5283111 – Digital, Addressable Fire-Alarm System ← Division 28

3. Trade Specific Scope of Work Items

  1. 1.Complete hydraulically calculated design by a NICET Level III or higher designer, including full submittal to the authority having jurisdiction…
  2. 2.Temporary standpipes maintained in service throughout construction per NFPA 241, OSHA and the requirements of the authority having jurisdiction.

4. Trade Specific Scope Exclusions

  1. 1.Fire alarm system, panel and devices, which are by the Division 28 Subcontractor. Furnishing tamper and flow switches for their connection is included.

Abridged — a generated Division 21 scope is 72 numbered items. Build one yourself, or read the full exported exhibit.

Built to be run in production

📄

Real PDFs

WeasyPrint paged media: running headers and footers, Page N of M, proper page breaks, and text that is selectable and searchable — not a screenshot.

📝

Word you can redline

python-docx output with bold, italic and underline preserved as character runs, and live page-number fields. Contracts get marked up in Word.

🗂️

Versioned and auditable

Issuing a scope freezes an immutable snapshot. Later edits create a new version, so the text that went out with the subcontract is preserved verbatim.

👥

Organizations and roles

Viewer, editor and admin; invitations; optional OIDC single sign-on. Every record is tenant-scoped, and cross-tenant access returns 404.

🔌

Connects to what you use

Optional integrations, off by default. Import projects and bid packages, and push finished exhibits back. Credentials stay server-side and are encrypted at rest.

🛡️

Two-factor and an audit log

TOTP with single-use recovery codes; the enrolment QR is an inline SVG so the shared secret never leaves the server. Every privileged action is recorded in an append-only log that outlives the account that did it.

Built to take load

Exports are cached by document fingerprint and can be rendered on a worker instead of in the request. Prometheus metrics at /metrics. The load test runs against PostgreSQL with 8 concurrent clients on every push.

💾

Backups that have been restored

CI dumps a populated database, drops the schema, restores it, and checks the result renders the same exhibit byte for byte. A backup procedure nobody has restored from is a hypothesis.

⌨️

Usable without a mouse

The outline reorders from the keyboard, not only by dragging, and announces each move. Export everything held about your account, or delete it — with the audit trail and your colleagues’ documents deliberately preserved.

🔒

No CDN, tight CSP

Every asset is served by the app, so default-src 'self' holds and it runs on an air-gapped jobsite network. Argon2id passwords, Fernet-encrypted tokens.

Where this came from

Earlier versions of this repository were a browser-only prototype whose JavaScript was never committed. v1.0.0 was a complete rewrite; everything since has been hardening.

ConcernPrototypeToday
Does it run?No — the four referenced js/*.js files were never committedFlask 3 app, 9 blueprints, service layer, 439 tests
OAuth secretStored in browser localStorageServer-side only; tokens Fernet-encrypted at rest
PDF exporthtml2canvas screenshot pasted onto one A4 pageWeasyPrint paged media, selectable text, Page N of M
DOCX exportalert('would be implemented here')python-docx with formatting runs and page fields
Scope contentOne hardcoded Fire Protection sample236 clauses, 139 spec sections, 20+ trades
CSI divisions16 hand-typed, several of which do not existCanonical MasterFormat 2020; reserved numbers excluded
StorageBrowser localStoragePostgreSQL, SQLAlchemy 2.0, Alembic migrations
AccountsNoneOrganizations, roles, invitations, OIDC SSO, two-factor, audit log

Get started

Docker is the shortest path — the PDF stack is already in the image.

git clone https://github.com/ibuilder/scopemaker.git
cd scopemaker
cp .env.example .env

# generate the two required secrets into .env
python -c "import secrets; print('SECRET_KEY=' + secrets.token_urlsafe(64))"
python -c "from cryptography.fernet import Fernet; print('ENCRYPTION_KEY=' + Fernet.generate_key().decode())"

docker compose up --build

Then create an account and some sample content:

docker compose exec web flask create-user you@example.com --org acme --role admin
docker compose exec web flask demo-data --org acme
Running without Docker? PDF export needs WeasyPrint's native libraries (Pango, cairo, GDK-PixBuf). Install them first, or PDF export stays disabled while every other format keeps working. Run flask check-pdf to see where you stand — the deployment guide has the per-platform commands.

README

Overview, quick start and the full feature list.

Deployment

Secrets, PostgreSQL, reverse proxy, health checks, upgrades.

Clause library

The YAML format, cross-references, and customising it for your company.

JSON API

Token auth, generating a scope in one call, exports.

Architecture

The document model, numbering, rendering and security posture.

Integrations

Optional connectors — Procore and OIDC single sign-on. Off by default.

Issues

Bugs, ideas and clause library contributions are welcome.