Project management that helps teams learn and improve
This is a fork of US-Department-of-the-Treasury/ship, audited for the GauntletAI ShipShape sprint. The upstream product documentation continues below and is unchanged.
| Deliverable | Location |
|---|---|
| Audit report — 68 findings across 8 categories, with methodology, raw data and severity ranking | audit/AUDIT_REPORT.md |
| Codebase orientation — architecture write-up, traced request flow, 10× assessment | audit/ORIENTATION.md |
| Per-category baselines — machine-readable, used for before/after comparison | audit/<category>/baseline.json + baseline.md |
Baseline: commit 076a183, measured 2026-07-27 against 500 documents / 20 users on PostgreSQL 15-alpine.
Findings: 4 Critical · 22 High · 29 Medium · 13 Low, across type safety, bundle size, API response time, database queries, test quality, error handling, accessibility, and Terraform/IaC.
No application or infrastructure source (api/, web/, shared/, terraform/) was modified during the audit phase — measurements reflect the commit above.
git clone https://github.com/US-Department-of-the-Treasury/ship.git
cd ship
./start.shThat is the whole thing, from a genuinely clean checkout: it installs dependencies if needed, creates
the database, runs every migration (and independently verifies the count — see DB-1 below, not just
trusts the exit code), seeds sample data, finds free ports, starts both servers, and prints the URLs
to open. Re-running ./start.sh is safe — every step is idempotent, so it heals a partially-set-up
checkout instead of assuming yesterday's state is still correct. Ctrl-C stops both servers.
./start.sh is a thin preflight (Node/pnpm present) wrapping scripts/dev.sh, which does the actual
work and is also what pnpm dev runs — there is exactly one implementation of "set up and start Ship,"
not two that can drift apart.
Postgres. By default this assumes a native PostgreSQL on localhost (no password) — the common
Homebrew/apt setup. No native Postgres installed? Bring up one of the two bundled Docker options first,
then point start.sh at it with DATABASE_URL:
# Option A — root docker-compose.yml (Postgres only, port 5432)
docker compose up -d
DATABASE_URL=postgresql://ship:ship_dev_password@localhost:5432/ship_dev ./start.sh
# Option B — docker-compose.local.yml (Postgres only, port 5433 — coexists with a native install)
docker compose -f docker-compose.local.yml up -d postgres
DATABASE_URL=postgresql://ship:ship_dev_password@localhost:5433/ship_dev ./start.shIf Postgres isn't reachable at all, ./start.sh fails immediately and says exactly what to start —
it does not proceed to start servers against a database it never confirmed existed.
pnpm db:migrateused to stop after migration 010 and still exit0. Fixed (DB-1 / TRO-178): the runner now throws — and./start.sh/pnpm devindependently re-verify every migration file on disk is recorded inschema_migrations, printingMigrations: 42/42 applied. Confirmed in this tree by runningrunMigrations()against the real 42-file migration set inapi/src/db/__tests__/migrationRunner.test.tsandverifyMigrations.test.ts.pnpm testTRUNCATEs whateverDATABASE_URLpoints at (api/src/test/setup.ts, every api test file'sbeforeAll) — including the database./start.sh/pnpm devjust set up. Never runpnpm testagainst your dev database; give it an isolated one (.factory-envdoes this for factory worktrees). (TEST-9, still open.)- Root
pnpm testruns both packages (test:apithentest:web) — this used to silently skipweb/entirely; that is fixed (TEST-1 / TRO-223, PR #11). Usepnpm test:webto run the web suite alone. ./start.sh/pnpm devpick their own ports and write them to a repo-root.portsfile — don't assume 3000/5173; read the printed URLs or.ports.
Ship is a project management tool that combines documentation, issue tracking, and plan-driven weekly workflows in one place. Instead of switching between a wiki, a task tracker, and a spreadsheet, everything lives together.
Built by the U.S. Department of the Treasury for government teams, but useful for any organization that wants to work more effectively.
Ship has four main views, each designed for different questions:
| View | What it answers |
|---|---|
| Docs | "Where's that document?" — Wiki-style pages for team knowledge |
| Issues | "What needs to be done?" — Track tasks, bugs, and features |
| Projects | "What are we building?" — Group issues into deliverables |
| Teams | "Who's doing what?" — See workload across people and weeks |
- Create documents for anything your team needs to remember — meeting notes, specs, onboarding guides
- Create issues for work that needs to get done — assign them to people and track progress
- Group issues into projects to organize related work
- Write weekly plans to declare what you intend to accomplish each week
Everyone on the team can edit documents at the same time. You'll see other people's cursors as they type.
In Ship, there's no difference between a "wiki page" and an "issue" at the data level. They're all documents with different properties. This means:
- You can link any document to any other document
- Issues can have rich content, not just a title and description
- Projects and weeks are documents too — they can contain notes, decisions, and context
Ship is plan-driven: each week starts with a written plan declaring what you intend to accomplish and ends with a retro capturing what you learned. Issues are a trailing indicator of what was done, not a leading indicator of what to do.
- Plan (Weekly Plan) — Before the week, write down what you intend to accomplish and why
- Execute (The Week) — Do the work; issues track what was actually done
- Reflect (Weekly Retro) — After the week, write down what actually happened and what you learned
This isn't paperwork for paperwork's sake. Teams that skip retrospectives repeat the same mistakes. Teams that write things down learn and improve.
Documentation requirements in Ship are visible but not blocking. You can start a new week without finishing the last retro. But the system makes missing documentation obvious — it shows up as a visual indicator that escalates from yellow to red over time.
The goal isn't to check boxes. It's to capture what your team learned so you can get better.
# 1. Clone the repository
git clone https://github.com/US-Department-of-the-Treasury/ship.git
cd ship
# 2. One command: installs deps, creates + migrates + seeds the database,
# starts both servers. See "Cold start" above for Docker Postgres options.
./start.shPrefer to run the steps yourself? ./start.sh (and pnpm dev) do the equivalent of:
pnpm install
pnpm build:shared
DATABASE_URL=... pnpm --filter @ship/api db:migrate # applies schema.sql + every migration
DATABASE_URL=... pnpm --filter @ship/api db:seed # idempotent — safe to re-run
pnpm devOnce it's running, open your browser to:
Log in with the demo account:
- Email:
dev@ship.local - Password:
admin123
| Service | URL | Description |
|---|---|---|
| Web app | http://localhost:5173 | The Ship interface |
| API server | http://localhost:3000 | Backend services |
| Swagger UI | http://localhost:3000/api/docs | Interactive API documentation |
| OpenAPI spec | http://localhost:3000/api/openapi.json | OpenAPI 3.0 specification |
| PostgreSQL | localhost:5432 | Database (via Docker) |
pnpm dev # Start everything
pnpm dev:web # Start just the web app
pnpm dev:api # Start just the API
pnpm db:seed # Reset database with sample data
pnpm db:migrate # Run database migrations
pnpm test # Run testsShip is a monorepo with three packages:
- web/ — React frontend with TipTap editor for real-time collaboration
- api/ — Express backend with WebSocket support
- shared/ — TypeScript types used by both
| Layer | Technology |
|---|---|
| Frontend | React, Vite, TailwindCSS |
| Editor | TipTap + Yjs (real-time collaboration) |
| Backend | Express, Node.js |
| Database | PostgreSQL |
| Real-time | WebSocket |
- Everything is a document — Single
documentstable with adocument_typefield - Server is truth — Offline-tolerant, syncs when reconnected
- Boring technology — Well-understood tools over cutting-edge experiments
- E2E testing — 73+ Playwright tests covering real user flows
See docs/application-architecture.md for more.
ship/
├── api/ # Express backend
│ ├── src/
│ │ ├── routes/ # REST endpoints
│ │ ├── collaboration/ # WebSocket + Yjs sync
│ │ └── db/ # Database queries
│ └── package.json
│
├── web/ # React frontend
│ ├── src/
│ │ ├── components/ # UI components
│ │ ├── pages/ # Route pages
│ │ └── hooks/ # Custom hooks
│ └── package.json
│
├── shared/ # Shared TypeScript types
├── e2e/ # Playwright E2E tests
└── docs/ # Architecture documentation
# Run all E2E tests
pnpm test
# Run tests with UI
pnpm test:ui
# Run specific test file
pnpm test e2e/documents.spec.tsShip uses Playwright for end-to-end testing with 73+ tests covering all major functionality.
| Environment | Approach |
|---|---|
| Development | ./start.sh / pnpm dev — see "Cold start" above |
| Shadow (UAT) | Deploy from feat/unified-document-model-v2 before merging to master |
| Production | AWS — Elastic Beanstalk (backend) + S3/CloudFront (frontend), provisioned via Terraform |
./scripts/deploy.sh prod # Backend → Elastic Beanstalk
./scripts/deploy-frontend.sh prod # Frontend → S3/CloudFrontRun both — they're paired; deploying only one leaves the API and frontend out of sync.
curl can't catch JS errors, so verify with a browser. Health checks:
- Prod API:
https://ship.awsdev.treasury.gov/health— goes through CloudFront, not a direct ALB hit.terraform/security-groups.tfrestricts the ALB security group to CloudFront's origin-facing prefix list, so a direct connection to the ALB's own DNS name (ship-api-prod.eba-xsaqsg9h.us-east-1.elasticbeanstalk.com) will time out once that's live — the name still resolves, but traffic from outside CloudFront's IP ranges is silently dropped. - Prod Web:
https://ship.awsdev.treasury.gov
Ship's Week 6 platform work (PLUGFORGE.MD) adds a versioned public API (/api/v1) authenticated via
OAuth 2.0. Per the same repo convention as the web app's grader login
(alice.chen@ship.local / admin123 — see FLEETGRAPH.MD's "Grader Access" section for that
account), the public API gets its own seeded, read-only grader credential — a first-party OAuth app
scoped to documents:read, issues:read, sprints:read only, so a grader account can read every
graded resource and mutate nothing.
One-command setup, from a clean checkout, alongside the normal ./start.sh / pnpm dev flow:
GRADER_OAUTH_CLIENT_SECRET=<choose-a-secret-value> ./start.sh
# or, against an already-running dev environment:
GRADER_OAUTH_CLIENT_SECRET=<choose-a-secret-value> pnpm --filter @ship/api db:seeddb:seed is idempotent (safe to re-run, including with the variable unset — see below) and prints
the app's client_id on success:
✅ Created grader OAuth app (client_id: ship_app_...)
or, on a re-run against an already-seeded database:
ℹ️ Grader OAuth app already exists (client_id: ship_app_...)
GRADER_OAUTH_CLIENT_SECRETis the raw client secret for the app — chosen by whoever runs the seed, never generated or printed by it, and never committed anywhere in this repo. Ship stores only its SHA-256 hash (oauth_apps.client_secret_hash, the same at-rest pattern as every other OAuth app and personal API token). Keep the value you chose; it is not recoverable from the database or re-printed on a later seed run.- Not set?
db:seedskips the grader app step entirely (no error, no row created) — this is the normal, unaffected path for every ordinary localpnpm db:seed/./start.shrun. The variable only needs to be set in an environment meant to actually host the grader's credential (a deployed grading instance's boot environment, provisioned via Terraform — PF-900).
Try it — mint a token and call the API. No browser, no login, no portal — the grader's
credential uses the OAuth 2.0 Client Credentials grant (POST /oauth/token), which trades a
client_id/client_secret directly for a bearer token in one call. Paste this whole block (fill
in the client_id the seed step printed and the secret you chose for
GRADER_OAUTH_CLIENT_SECRET):
API_URL=http://localhost:3000 # or your deployed instance's URL
CLIENT_ID=ship_app_grader_... # from the seed step's printed output
CLIENT_SECRET=... # whatever you set GRADER_OAUTH_CLIENT_SECRET to
TOKEN=$(curl -s -X POST "$API_URL/oauth/token" \
-d 'grant_type=client_credentials' \
-d "client_id=$CLIENT_ID" -d "client_secret=$CLIENT_SECRET" \
-d 'scope=documents:read issues:read sprints:read' \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])')
curl -s "$API_URL/api/v1/me" -H "authorization: Bearer $TOKEN" | python3 -m json.tool
curl -s "$API_URL/api/v1/documents?limit=5" -H "authorization: Bearer $TOKEN" | python3 -m json.tool
curl -s "$API_URL/api/v1/issues?limit=5" -H "authorization: Bearer $TOKEN" | python3 -m json.toolGET /api/v1/me confirms the principal: {"user": null, "app": {"name": "Grader (read-only)", ...}, "scopes": ["documents:read","issues:read","sprints:read"]} — an app identity, no human attached,
exactly the three read scopes and nothing else. The credential is provably read-only, not just
labeled that way — any write is rejected with the missing scope named, never a bare "forbidden":
curl -s -X POST "$API_URL/api/v1/documents" -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"title":"test","document_type":"wiki"}'
# → 403 {"code":"forbidden","message":"...","details":{"missing_scope":"documents:write"}}The full public surface is browsable at GET /api/v1/openapi.json (no auth required) — every
route the token above can reach, plus request/response schemas.
- Developer portal (E5, PF-502/PF-503) — the click-through path. The portal is real UI inside
the normal Ship shell (it consumes
/api/v1like any third-party client — seePLUGFORGE.MD§2.9). To reach it: log in as any user (e.g.alice.chen, credentials above) → click Developer in the icon rail (bottom, above Settings) → you land on/developer/apps: register an app, see its client secret exactly once (shown-once modal with copy button, never re-fetchable), open the app to rotate the secret or revoke → the sidebar's Webhooks entry (/developer/webhooks) has subscription CRUD, the delivery log (server-side pagination + status filter, including "Dead (DLQ)"), and a Replay button per delivery that re-emits under the originalIdempotency-Key. Playwright covers both screens (e2e/developer-portal-apps.spec.ts,e2e/developer-portal-dlq-replay.spec.ts). SeePLUGFORGE.MD§4 (PF-907) and §6 (MVP cut line) for the full epic breakdown.
Working credentials for the deployed site, same disclosure basis as alice.chen's web-login
credentials above (FLEETGRAPH.MD's "Grader Access" section): deliberately published, because the
grading brief requires a working credential, this deployment is a graded test environment, and the
credential is scoped read-only to synthetic seed data only — nothing here grants write access or
reaches anything real. Verified 2026-08-15 by minting a real token and calling the live API.
| Field | Value |
|---|---|
| API URL | https://ship-rr6m.onrender.com |
client_id |
ship_app_grader_9ea6a33b |
client_secret |
4137c69bb19691aadfcec95cf686d2c95eec38157f4d9054a562b01ff0a853eb |
| Scopes | documents:read issues:read sprints:read (read-only, enforced — see the 403 example above) |
API_URL=https://ship-rr6m.onrender.com
CLIENT_ID=ship_app_grader_9ea6a33b
CLIENT_SECRET=4137c69bb19691aadfcec95cf686d2c95eec38157f4d9054a562b01ff0a853eb
TOKEN=$(curl -s -X POST "$API_URL/oauth/token" \
-d 'grant_type=client_credentials' \
-d "client_id=$CLIENT_ID" -d "client_secret=$CLIENT_SECRET" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])')
curl -s "$API_URL/api/v1/documents?limit=5" -H "authorization: Bearer $TOKEN" | python3 -m json.tool| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Required |
SESSION_SECRET |
Cookie signing secret | Required |
PORT |
API server port | 3000 |
GRADER_OAUTH_CLIENT_SECRET |
Raw secret for the seeded read-only grader OAuth app (see "Grader Access — Public API" above) | Unset — seed step no-ops without it |
- No external telemetry — No Sentry, PostHog, or third-party analytics
- No external CDN — All assets served from your infrastructure
- Session timeout — 15-minute idle timeout (government standard)
- Audit logging — Track all document operations
Reporting Vulnerabilities: See SECURITY.md for our vulnerability disclosure policy.
Ship is Section 508 compliant and meets WCAG 2.1 AA standards:
- All color contrasts meet 4.5:1 minimum
- Full keyboard navigation
- Screen reader support
- Visible focus indicators
We welcome contributions. See CONTRIBUTING.md for guidelines.
- Application Architecture — Tech stack and design decisions
- Unified Document Model — Data model and sync architecture
- Document Model Conventions — Terminology and patterns
- Week Documentation Philosophy — Why weekly plans and retros work the way they do
- Accountability Philosophy — How Ship enforces accountability
- Accountability Manager Guide — Using approval workflows
- PlugForge Pre-Search (Week 6) — The three-phase pre-search checklist for the platform work, pre-filled from the PRD and repo with
file:linecitations;[TROY — needs your answer]blocks mark the personal answers still owed - Contributing Guidelines — How to contribute
- Security Policy — Vulnerability reporting