Growing from ReeWeb
ReeWeb and Reepolee are two sides of the same coin. They share the same .ree template engine, the same translation key conventions and lookup tags, the same Tailwind CSS approach, and the same Bun-native philosophy - zero runtime dependencies, plain TypeScript, no framework lock-in.
The difference is scope. ReeWeb is a static site generator. It produces flat HTML files that you deploy to a CDN. The Reepolee Framework is a full-stack Bun framework and application foundation. It runs as a Bun process with a database, authentication, form handling, email, and an admin panel.
Many of our clients start with ReeWeb for a marketing site, a documentation hub, or a multi-language landing page. And that is the right call - static files are faster, simpler, and cheaper to serve than a server process. But as the project grows, hand-editing content files stops scaling. The answer is not to migrate the site into something bigger. You add a Reepolee application next to the ReeWeb project and use it as the source of data - the way you would use a CMS or a DXP - while the site itself stays static.
The Continuum
ReeWeb and Reepolee are not separate products with separate ecosystems. They grow from the same foundation and divide the work between them:
ReeWeb (static site) Reepolee (application)
----------------------------------------------------------
Renders pages at build time Stores and serves the data
Flat HTML on a CDN Database-backed Bun process
JSON translation files `translations` database table
No auth, no forms Auth, forms, admin, email
The shared foundation is the template syntax, the components model, the translation key conventions, and the styling. The storage is deliberately different on each side: ReeWeb keeps its translation values in per-directory {lang}.json files that are baked into the static build, while Reepolee keeps its translations in the translations database table, served at request time. Neither store replaces the other, and nothing needs to be converted - each product uses the store that fits its runtime.
When to Add Reepolee
ReeWeb alone is the right choice when:
- Your content is mostly static (documentation, blog, marketing pages)
- You don't need user accounts or personalised content
- Your forms are simple - a contact or inquiry form is a small addition to the site's Cloudflare Worker, which sends the submission by email or writes it to D1
- You want the simplest possible deployment (copy files to a CDN)
Adding a Reepolee application next to it becomes the right choice when:
- You need to store data - team members, projects, product records that pages are built from
- You need an admin panel - a place for staff to manage content without editing markdown files
- You need authentication - login, registration, password resets, role-based access
- You need forms tied to your data - order forms that create records, workflows with validation, database writes, and email follow-up (a simple contact form doesn't need this - it belongs in the site's worker)
- You need file uploads - avatars, images, documents that users submit
- You need truly dynamic pages - pages that change based on who's viewing them
These are not either/or decisions. The most common combination keeps the whole public site in ReeWeb - static, cached, CDN-served - and puts the content management, data storage, and application features in the Reepolee app beside it.
Running Them Side by Side
The two projects live next to each other, each with its own repository and deployment:
labs/
├── my-site/ # ReeWeb - the public static site
└── my-app/ # Reepolee - data, admin, application features
ReeWeb ships a small client for Reepolee's read API in src/lib/reepolee_api.ts:
fetch_collection(route_path, opts?) // → { data, total, limit, offset }
fetch_record(route_path, id) // → record | null
Any page's data loader can pull records from the Reepolee app at build time:
// src/public/team/index.ts (sibling to index.ree)
import { fetch_collection } from "../../lib/reepolee_api";
export async function load_template_data(): Promise<Record<string, any>> {
let team: any[] = [];
try {
const team_result = await fetch_collection("/team");
team = team_result.data;
} catch (err) {
console.warn("[reeweb] Could not fetch team from local reepolee server:", (err as Error).message);
}
return { team };
}
The template renders the records like any other props data:
{#each props.team as member}
<article>{= member.full_name }</article>
{/each}
The wiring takes two steps:
- Start Reepolee in agent mode:
bun run agent. Agent mode is a development-only mode that binds to127.0.0.1:AGENT_SERVER_PORTand answersAccept: application/jsonrequests on its generated CRUD routes with a{ data, total, limit, offset }envelope (single records viafetch_record). Sensitive fields are stripped from JSON responses, and routes without JSON support answer with a 404 envelope. Protected routes authenticate through theAGENT_USER_USERNAMEenv var. - Point ReeWeb at it: set
REEPOLEE_API_URL=http://localhost:2500(the ReepoleeAGENT_SERVER_PORT) in the ReeWeb project's.env.
From then on, bun run dev fetches live data on every request and bun run ssg fetches it once per page at build time. The published site is still plain static files - Reepolee is consulted during the build, not by visitors. Fetch failures are caught and logged, and the page still renders with an empty data set, so an unreachable backend never breaks the build.
Static pagination works with this data too: register the route in config/pagination.ts, export load_records(lang) from the route's index.ts, and the build chunks the Reepolee records into /team/2/-style pages. See Pagination for the mechanics.
This is the CMS/DXP shape: staff create and edit records in Reepolee's generated admin panels, and the next site build turns those records into static HTML. Content changes go live by rebuilding the site, which is the same publish step every static site already has.
Translations Stay Put
Translation handling is the area where the two products are most alike in the templates and most different in storage - and the side-by-side model means nothing moves:
- ReeWeb loads translations from per-directory
{lang}.jsonfiles at build time. Key paths are namespaced by directory, and the values ship inside the rendered HTML. - Reepolee loads translations from the
translationsdatabase table at server startup. Every translation is a row(locale, namespace, key_path, translation), edited through SQL, the/system/translationsadmin UI, or the AI-powered sync generator.
Both engines resolve the same lookup tags - {_ path } (escaped), {- path } (unescaped), {@ path } (markdown) - against the same kind of key paths, with the same {last_segment} missing-key marker. A translator or developer who learned the key conventions on one side already knows the other. But the values live where each product needs them: JSON files for the static site, database rows for the application. There is no import step because there is nothing to import.
What Each Side Owns
| Concern | ReeWeb site | Reepolee application |
|---|---|---|
| Public pages | .ree / .md under src/public/ | Server-rendered routes under routes/ |
| Components | src/components/ | components/ |
| Translation values | {lang}.json per directory | Rows in the translations table |
| Content records | Fetched at build time | Stored, validated, and edited in the DB |
| Content editing | Markdown files, or Reepolee's admin | Generated admin panels |
| Deployment | Static files on a CDN | Bun process on a VPS or container |
The shared foundation - .ree syntax, the components model, translation key conventions, Tailwind setup - is the reason the two projects feel like one system even though they deploy separately.
What Reepolee Adds
The Reepolee app beside your site brings the application features that static files cannot provide:
| Capability | How Reepolee provides it |
|---|---|
| Database | SQLite or MySQL. Bun's native SQL API. No ORM. |
| Auth | Session-based. Login, invite-only registration, password reset. Role tags (user, admin, etc.) for access control. |
| Forms | Zod-validated, server-side with live client feedback. Toast notifications on success. |
| Admin panel | Generated from your database schema. List, search, create, edit, delete - with navigation, module gating, and translations. |
| SMTP-based transactional email (welcome, password reset, notifications). | |
| File uploads | S3-compatible storage (MinIO, R2, AWS S3) with local-filesystem fallback. |
| Background work | Redis-backed job queue with a standalone worker process - email sending, scheduled jobs, long-running tasks. |
You add these piece by piece, in whatever order your project needs them. The public pages that don't need any of it stay in ReeWeb and render the way they always did.
Same Templates, Different Context
The .ree syntax is identical, and so is the data prefix - both reference render data through props.:
<!-- ReeWeb template -->
<h1>{= props.title }</h1>
<!-- Reepolee template - same prefix -->
<h1>{= props.title }</h1>
The only difference is what's available in the render context. Reepolee templates have access to server-side globals that a static build cannot have - the current user, toast notifications, the authenticated session:
{#if props.user }
<p>Welcome back, {= props.user.display_name }.</p>
{/if}
Components are invoked the same way in both - a ReeTag whose attributes arrive under props.attributes and whose slot content arrives as props.children. So if a project ever grows a page that genuinely needs per-user rendering, that page can be built in the Reepolee app with the same syntax, the same component patterns, and the same translation key conventions you already use on the site. That is reuse of a shared foundation, not a migration.
Growth, Not Migration
The relationship between the two tools is additive. You start with ReeWeb, launch, get traffic, and learn what your users actually need. When the requirements arrive - content editing, data, accounts, forms - you add a Reepolee application next to the site and let it do what a CMS or DXP would do, with the same templates, the same key conventions, and none of the third-party lock-in. The site stays static and fast; the application grows beside it at its own pace.
For a direct comparison of the two tools and guidance on which fits your current project, see the ReeWeb & Reepolee page.