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.
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.The Scope of Work shall include the furnishing and installation, complete, of the FIRE PROTECTION Work in accordance with the Contract Documents.
- 2.This shall include all required engineering, submittals, materials, fabrication, labor, equipment, supervision, hoisting, licenses, burden, taxes and insurances…
- 3.The Scope of Work shall include all Division 01 General Requirements and the Work in the following Specification Section(s):
- 3.1211313 – Wet-Pipe Sprinkler Systems
- 3.2213113 – Electric-Drive Centrifugal Fire Pumps
- 3.3078413 – Penetration Firestopping ← Division 07
- 3.4083113 – Access Doors and Frames ← Division 08
- 3.5283111 – Digital, Addressable Fire-Alarm System ← Division 28
3. Trade Specific Scope of Work Items
- 1.Complete hydraulically calculated design by a NICET Level III or higher designer, including full submittal to the authority having jurisdiction…
- 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.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.
| Concern | Prototype | Today |
|---|---|---|
| Does it run? | No — the four referenced js/*.js files were never committed | Flask 3 app, 9 blueprints, service layer, 439 tests |
| OAuth secret | Stored in browser localStorage | Server-side only; tokens Fernet-encrypted at rest |
| PDF export | html2canvas screenshot pasted onto one A4 page | WeasyPrint paged media, selectable text, Page N of M |
| DOCX export | alert('would be implemented here') | python-docx with formatting runs and page fields |
| Scope content | One hardcoded Fire Protection sample | 236 clauses, 139 spec sections, 20+ trades |
| CSI divisions | 16 hand-typed, several of which do not exist | Canonical MasterFormat 2020; reserved numbers excluded |
| Storage | Browser localStorage | PostgreSQL, SQLAlchemy 2.0, Alembic migrations |
| Accounts | None | Organizations, 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
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.