OpenProductKit is a white-label, full-stack template for shipping commercial apps across web, CLI and desktop from one decoupled core.
To prove the pitch, I generated a project from it and reworked that generated app into Tally, a real freelancer time tracker, using an AI coding agent that followed nothing but the generated project's own AGENTS.md.
The repo history is the tutorial: the first commit is the pristine template output, and every commit after it is one layer of the rework.
The gap between a boilerplate and a product
Every developer has a folder full of app ideas. Most die in the same place: not at the idea, but at the product part.
The licensing. The desktop packaging. The typed API client that drifts out of sync. The migrations. The "how do I rebrand this template without a find-and-replace mess" problem.
Boilerplates solve day one. Almost none solve week four, when you need to sell a license key, ship a Windows build, or pull upstream template fixes into a project you have heavily customized.
And in 2026 there is a question classic boilerplates were not designed to answer:
What does your AI coding agent do with the template?
You are probably not hand-writing every route and component anymore. You generate a project, open Claude Code, Cursor or Codex, and say: "build my idea into this." Most templates give the agent no rules, no map, and no replacement recipe, so it guesses. The architecture starts eroding from the first commit.
OpenProductKit is built to close both gaps.
What you start from
OpenProductKit is a Copier template. One command, a few questions, and you get a branded monorepo:
-
One decoupled core:
packages/coreis pure Python with dataclasses, ports and services. No FastAPI, no database, no HTTP. - Thin adapters: FastAPI, Typer, React and pywebview sit around the same business logic.
- Desktop without a sidecar: the same React UI runs in a native window and calls the core in-process over a JS bridge.
- Real licensing: Ed25519-signed offline tokens, vendor tooling, route gates and lock-card UI.
- Plugins: Python entry-point plugins with license gating and an admin UI.
- Generated typed client: OpenAPI to TypeScript, so backend drift becomes a compile error.
- Migrations, tests, docs and CI: all rendered into the generated project.
-
AGENTS.mdandCLAUDE.md: pre-rendered with your package names, CLI command and architecture rules.
The template ships a deliberately small demo domain, a notes-and-tags "Resource Vault", threaded through every layer. Every demo line is fenced with a grep-able [demo] marker. It exists to be deleted.
The proof: building Tally
Tally is a local-first time tracker for freelancers:
- Clients with hourly rates
- One running timer at a time
- Recent time entries
- Weekly billable reports
- CSV export as a paid feature
- Web, CLI and desktop surfaces
- Same core, same data
Here is the generated app after replacing the demo with realistic Tally data:
And the weekly billable report:
Step 0: generate the project
The first commit is just the output of the template:
uvx copier copy gh:ravipurohit1991/OpenProductKit tally
# project_name=Tally, cli_name=tally, pkg_slug=tally
That initial project already runs. It has a web UI, CLI, desktop shell, licensing, plugins, migrations, docs, CI and tests. That matters because it makes the rest of the history honest: everything after commit zero is the product rework.
Step 1: let the agent read AGENTS.md
The generated AGENTS.md opens with the rule that keeps the codebase sane:
Business logic never leaves
packages/core.
FastAPI, Typer, React and pywebview are delivery mechanisms. If a product decision is encoded inside a route, CLI command or React component, it belongs in a core service instead.
The file also gives the agent a layer-by-layer recipe:
- Add the new core domain.
- Add ports and services.
- Test the core with in-memory fakes.
- Add persistence.
- Add API routes.
- Regenerate the typed client.
- Replace the frontend.
- Replace CLI commands.
- Rework plugins.
- Delete every
[demo]marker.
This is the important bit: the agent is not guessing the architecture. It is following the project's own instruction manual.
Step 2: build the core first
Tally's product rules live in the core package:
ClientTimeEntryTimerServiceReportService
The central rule is simple but important: only one timer can run at a time.
That rule is enforced in the core, so the web UI, CLI and desktop app cannot disagree. The CLI does not need its own timer logic. The React app does not need its own timer logic. The API route does not need its own timer logic. They all call the same service.
Core tests run against in-memory fakes, so the most important behavior can be tested before touching FastAPI, SQLModel, React or pywebview.
Step 3: add persistence and API
After the core exists, the backend becomes an adapter:
- SQLModel rows
- Repositories implementing the core ports
- Alembic migrations
- FastAPI routes over the services
The paid CSV export is just a route gate:
@router.get("/export", dependencies=[Depends(require_plan("pro"))])
def export_report_csv(...) -> Response:
...
Tests cover both sides of the gate: the dev license stub grants pro, while a free plan returns a structured 403.
Step 4: regenerate the typed frontend client
After changing the API, the agent regenerates the frontend schema:
uv run tally gen
The React UI uses types derived from the backend OpenAPI schema. If an endpoint changes and the UI call site is wrong, TypeScript catches it.
Tally's frontend is intentionally modest: a timer view, a report view, plugin settings and license status. The point is not to be flashy. The point is that the product can move from generated scaffold to working application without breaking the architectural line.
The plugin manager is still there too, now reworked for the Tally domain:
Step 5: make the CLI a real adapter
The CLI is not a second implementation. It is another adapter over the same services:
$ tally client add "Acme Corp" --rate 100
$ tally start "Acme Corp" "api integration"
Timer started for Acme Corp. api integration
$ tally status
Running: Acme Corp 0:03 api integration
$ tally stop
Stopped: Acme Corp 0:09
$ tally report --week
Client Hours Rate Amount
Acme Corp 0.00 100.00 0.25
TOTAL 0.00 0.25
The one-running-timer rule is enforced here too, because it comes from TimerService.
Step 6: delete the demo
This is where the [demo] markers pay off:
rg "\[demo\]" packages apps
The agent can find the entire old demo surface, delete pure-demo files, trim marked sections from mixed files, regenerate the typed client and keep the project clean.
After the purge, Tally had:
- 45 tests
- Zero
[demo]markers - No dead Resource Vault scaffold
- Web, CLI and desktop still sharing one core
The desktop app required no product work
This was the nicest validation.
Tally's desktop app did not need a separate rewrite. The pywebview shell already knows how to load the same frontend and dispatch requests into the FastAPI app in-process. No sidecar. No local port. No second backend.
Once the web UI and API were reworked, the desktop app came along for free:
uv run tally build web
uv run tally desktop
That is what the "core never assumed HTTP" rule buys you.
Dogfooding caught a real bug
Full honesty: the first CI run of the generated project failed.
The generated workflow file is copied verbatim because GitHub Actions syntax uses ${{ }}, which collides with Jinja templating. One desktop smoke-test step still contained an unrendered {{ pkg_slug }}_desktop.
Tally was the first generated project to run its own CI. It caught that bug quickly, and the fix went back upstream.
That is exactly why I wanted a real proof project instead of a polished demo video. Building something from your own template finds the bugs that documentation misses.
Try it with your own idea
The workflow Tally validated is:
1. uvx copier copy gh:ravipurohit1991/OpenProductKit my-product
2. Open it with your coding agent:
"Read AGENTS.md, then replace the demo with <my idea>."
3. Review the diffs layer by layer.
4. Ship.
The agent does not guess the architecture. It is told.
It does not hunt for the demo. It greps the fence.
It does not hand-write client types. The recipe tells it when to regenerate.
And you review commits shaped like product layers instead of one giant rewrite.
What it is not
OpenProductKit is intentionally lean and opinionated:
- SQLite, SQLModel and Alembic
- React and Vite
- Typer
- pywebview and PyInstaller
- Dev-time Python entry-point plugins
- Local/offline licensing primitives
It is not a multi-tenant SaaS starter with Stripe, teams, billing portals and Kubernetes. Runtime plugin installation is still on the roadmap. Code signing is documented, not solved.
But if your idea is a product that a customer downloads or logs into, with a license key, plugins, and web + CLI + desktop surfaces, this is the fastest honest path from template to shipped that I know how to build.
Links
- Template: github.com/ravipurohit1991/OpenProductKit
- Proof project: github.com/ravipurohit1991/tally-time-tracker
- Docs article: OpenProductKit blog draft
Both projects are MIT licensed.





Top comments (0)