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 grow from one shared foundation of templates, translation keys, styling, and Bun-native tools. ReeWeb ships flat HTML with translation values in JSON files; Reepolee runs a full-stack application with translation values in the database. Reepolee runs beside ReeWeb as its content backend.

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:

  1. Start Reepolee in agent mode: bun run agent. Agent mode is a development-only mode that binds to 127.0.0.1:AGENT_SERVER_PORT and answers Accept: application/json requests on its generated CRUD routes with a { data, total, limit, offset } envelope (single records via fetch_record). Sensitive fields are stripped from JSON responses, and routes without JSON support answer with a 404 envelope. Protected routes authenticate through the AGENT_USER_USERNAME env var.
  2. Point ReeWeb at it: set REEPOLEE_API_URL=http://localhost:2500 (the Reepolee AGENT_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}.json files at build time. Key paths are namespaced by directory, and the values ship inside the rendered HTML.
  • Reepolee loads translations from the translations database table at server startup. Every translation is a row (locale, namespace, key_path, translation), edited through SQL, the /system/translations admin 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

ConcernReeWeb siteReepolee application
Public pages.ree / .md under src/public/Server-rendered routes under routes/
Componentssrc/components/components/
Translation values{lang}.json per directoryRows in the translations table
Content recordsFetched at build timeStored, validated, and edited in the DB
Content editingMarkdown files, or Reepolee's adminGenerated admin panels
DeploymentStatic files on a CDNBun 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:

CapabilityHow Reepolee provides it
DatabaseSQLite or MySQL. Bun's native SQL API. No ORM.
AuthSession-based. Login, invite-only registration, password reset. Role tags (user, admin, etc.) for access control.
FormsZod-validated, server-side with live client feedback. Toast notifications on success.
Admin panelGenerated from your database schema. List, search, create, edit, delete - with navigation, module gating, and translations.
EmailSMTP-based transactional email (welcome, password reset, notifications).
File uploadsS3-compatible storage (MinIO, R2, AWS S3) with local-filesystem fallback.
Background workRedis-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.