Converting an Existing Site into a Theme
Turn a base44-generated app — or any standalone React/Vite site — into a publishable ThreeU theme: keep the design, swap the data layer for threeu-sdk, fill the storefront gaps, publish.
When to Use This
You already have a finished design — a base44 app, a Lovable/Bolt export, an agency build — and want it in the ThreeU marketplace as a theme merchants can install. The single rule: preserve the source design faithfully and replace its data plumbing with `threeu-sdk`. A theme that still calls base44 (or any non-ThreeU API) fails review; a theme whose look drifted is not what the merchant bought.
This page is written as a runbook an AI coding agent can follow end-to-end. Point the agent at this URL plus the full corpus at https://docs.threeu.app/llms-full.txt and give it the developer token; the phases below are the plan.
Ground Rules
- Survey before touching code. For every surface (products, services, prices/stats, cart, checkout, delivery, payments, leads, content) the merchant decides dynamic (live from the SDK) or static (kept as designed). Guessing wastes work and can strip a design the merchant wanted.
- Only ThreeU, only through the SDK. Remove every
@base44/sdkcall,base44Client,app-paramsand base44 auth page — static surfaces keep their markup, they do not keep a lingering fetch. - Never commit the developer token. Git-ignored
.env.threeuonly; never printed in full; never in the bundle. - Keep the design system intact — Tailwind config, custom CSS, fonts, colors, animations, component structure.
- Deliver a complete bilingual storefront, not a converted subset: gap-fill missing standard pages in the source's own design language, and retrofit AR/EN when the source is single-language (both survey-gated).
- Work in phases and write state down in a
THEME_CONVERSION.mdinside the source app so the job is resumable.
Phase 0 — Token
- Validate:
GET https://api.threeu.app/api/developer/mewithAuthorization: Bearer <token>→200 { kind: "developer", account: {…} }. A401means wrong/expired — stop. Sandbox has a separate database; validate against production unless told otherwise. - Save to
<app>/.env.threeuasTHREEU_DEVELOPER_TOKEN=…andTHREEU_API_URL=https://api.threeu.app; add.env.threeuto.gitignore. Confirm with the value redacted.
Name it .env.threeu, not .threeu.env
The bundler excludes .env* — a file named .threeu.env would be uploaded with your token.
Phase 1 — Recon
Map the app before changing it. Record everything in THEME_CONVERSION.md:
- Identity & stack:
package.json(base44 apps have@base44/sdk+@base44/vite-plugin), router, styling, any i18n system (useLanguage,pick(ar,en),dir="rtl"). Classify the language status: bilingual / partial / monolingual. - Data layer — every hit is a conversion site:
grep -rnE "@base44|base44Client|base44\.entities|base44\.integrations|base44\.auth|appParams|app-params" srcPhase 1 — Pages & Gap Analysis
Inventory src/pages/ and the router; classify each page:
| Class | Examples | Becomes |
|---|---|---|
| Storefront | home, products, product, collections, cart, checkout, services, search, offers | Theme pages on the SDK |
| Content | about, blog, careers, projects, contact, FAQ, legal | SDK content engine or static editable pages |
| Lead-gen | quote, contact, request-a-demo | useLead() |
| Drop | login, register, password reset, OAuth consent, account, e-services, any admin/dashboard route | Removed — ThreeU owns identity, checkout and administration |
Then diff the inventory against the canonical page set — storefront: home, collection/products, product, cart, checkout, order-status, optionally services, service, search, offers; content: about, contact, blog, careers, faq, legal. Every canonical page the source lacks is a Phase 2 gap-fill candidate. A catalog site with no cart, or a store with no about/FAQ, is the normal case.
Phase 2 — Survey the Merchant, Then Plan
Ask one decision per surface, grounded in the recon (name the actual pages/entities you found):
| Surface | Question | Sensible default |
|---|---|---|
| Products | Live catalog via useProducts, or the source's hardcoded list as static design? Detail pages with variants? | dynamic |
| Services | Dynamic (type: 'service') or static pages? | dynamic if the brand sells services |
| Numbers / prices / stats | Live, or keep static figures? | prices dynamic; hero stats static |
| Cart & checkout | Does the store sell online? If yes, add a real cart + checkout; if lead-gen only, route CTAs to a quote form | add when it sells |
| Delivery / payments | Live methods + rates at checkout, or n/a? | live when checkout exists |
| Lead / quote / contact forms | Submit via useLead so leads reach the brand, or keep e.g. WhatsApp-only? | useLead |
| Content | SDK content engine, static, or drop? | SDK with empty-states |
| Missing pages | Per gap: add (new build in the source design language) or skip? | add commerce-critical; offer content pages |
| Editable vs fixed | Which hero copy / images / colors become T* editable? | hero + primary colors |
| Languages / brand | Keep the AR/EN system, or retrofit the missing language? Brand name/logo from useBrand() or hardcoded? | retrofit; useBrand() |
Record every answer as dynamic | static | drop in THEME_CONVERSION.md — that table is the contract for Phase 4. Then plan the whole storefront from the capability list ([Commerce](/docs/themes-commerce), [Headless](/docs/themes-headless)) and mark each capability needed / n-a. Flag anything with no SDK equivalent as an explicit limitation; never invent an endpoint.
base44 → threeu-sdk Mapping
Import hooks/components from threeu-sdk/theme; the framework-agnostic client is ThreeuStorefront from threeu-sdk/storefront. Everything is brand-scoped by the provider — never pass a brand id from a theme.
| base44 | threeu-sdk |
|---|---|
Product.filter({published:true}, sort, limit) | useProducts({ limit }) → normalized StorefrontProduct[] |
Product.filter({type:'service'}) | useProducts({ type: 'service' }) |
Product.get(id) / by slug | useProduct({ id }) / useProduct({ slug }) — product, localized, options, selectedVariant, quantity, selectOption, setQuantity |
| category / collection lists | useCollections() |
Lead.create(data) | useLead().submit({ name, phone, email, message, source, utm, payload }) — phone or email required |
| booking / appointment create | useBooking().book({ serviceSlug, date, time, form }) |
| add to cart | useCart().addItem({ productId, variantId, quantity, price }) |
| place order | useCheckout().checkout({ items, shipping, paymentMethod, deliveryMethod, discountCode, totalAmount, currency }) — server recomputes fee + discount |
| coupon | useCoupon().apply(code, subtotal) — rejection is data, not a throw |
| delivery + live rate | useDeliveryMethods() + useDeliveryQuote().quote({ method, lat, lng, subtotal }) |
| payment methods | usePaymentMethods() |
Article/Blog.list() | storefront.content.blog() / useBlogPost(slug) |
| careers / FAQ / comparison / legal | content.careers(), useFaqs(), content.comparisons(), content.legal() |
| page meta / JSON-LD | content.meta(path) + <TJsonLd> |
| pixels / analytics | useTracker() — GA4-aligned events + Meta/TikTok/Snap/Google pixels + UTM |
| brand info / logo / palette | useBrand() |
| locale / currency | useLocale(), useCurrency(), <TMoney>, <TLocalizedText> |
address {lat,lng,address} | <TAddress> + useAddress() |
base44.auth.*, Login/Register/Reset pages, base44Client.js, app-params.js | Remove |
No clean equivalent — handle explicitly: file upload (Core.UploadFile) has no public storefront endpoint — drop it or reference the file in payload; arbitrary custom entities (Project, Certification, Branch) become static T*-editable pages; WhatsApp deep-links and client-side widgets stay.
Replit, Lovable, Bolt & Custom-Backend Sources
The phases above apply to any React site; only the mapping changes. A base44 app talks to base44 entities; a Replit or Lovable full-stack app usually ships its own server (Express or Hono in server/), a database (Drizzle/Prisma over Postgres or SQLite), shared/schema.ts, and a frontend that fetches /api/... with react-query. None of that ships in a theme — ThreeU is the backend.
Recon additions for these apps
- Inventory the server routes:
grep -rnE "app\.(get|post|put|delete)\(" server(Express) or the router files — every route is a data call to replace. - Inventory the client calls:
grep -rnE "fetch\(|apiRequest\(|useQuery\(|useMutation\(" client/src— eachqueryKey/ URL maps to one SDK hook. - Note what the server did beyond CRUD (price maths, availability, email/SMS, uploads, auth sessions). That logic either moves to the SDK's server-side behaviour (checkout recomputes totals; leads/bookings create records) or is an explicit limitation.
Mapping: app server → threeu-sdk
| Source pattern | threeu-sdk |
|---|---|
GET /api/products, /api/menu, /api/services | useProducts({ type, collection, search }) |
GET /api/products/:id or :slug | useProduct({ id }) / useProduct({ slug }) |
GET /api/categories | useCollections() |
POST /api/contact, /api/quotes, /api/inquiries, /api/newsletter | useLead().submit(...) (phone or email required) |
POST /api/bookings, /api/appointments, /api/reservations | useBooking().book({ serviceSlug, date, time, form }) |
local cart state, localStorage cart, /api/cart | useCart() / useAddToCart() |
POST /api/orders, /api/checkout, Stripe session creation | useCheckout() with useDeliveryMethods, usePaymentMethods, useCoupon, <TAddress> — the payment gateway is the brand's, configured in ThreeU |
/api/posts, /api/blog, /api/faq | content.blog(), useBlogPost(slug), useFaqs() |
/api/settings, /api/site (name, logo, colours, socials) | useBrand() — name, logo, banners, 60/30/10 palette, socials, support |
/api/upload, object storage | no public storefront upload — drop, or reference the file in a lead payload |
/api/auth/*, sessions, admin dashboard routes, /admin pages | Delete — ThreeU owns identity and administration |
Replit Secrets / process.env on the server | The theme has no server; the only secret is the developer token in .env.threeu (or a Replit Secret) used by npx threeu publish |
Structural changes
- Delete
server/,shared/schema.ts, the DB config and their dependencies; move the app to a plain Vite root (client/contents become the project root, or pointvite.configrootat it). - Replace the react-query
queryFnfetches with SDK hooks (keep react-query if the app uses it elsewhere — the SDK does not need it). - Keep the design system exactly as is: Tailwind config, shadcn components, fonts, animations.
- Non-React sources (plain HTML, Flask/Django templates, Streamlit) cannot become a theme as-is; the markup must be ported into React components first.
Working inside Replit's agent
- Store the developer token as a Replit Secret named
THREEU_DEVELOPER_TOKENand tell the agent to read it from the environment — never paste it into the chat or a file. - Give the agent the prompt at the end of this page. Hold it to the Phase 2 survey before it edits code, and to the Phase 5 checklist before it publishes.
- Publish from the Replit shell:
npm run build && npx threeu publish --dry-run && npx threeu publish(Node 18+).
Phase 3 — Scaffold the Theme Shell
npm install threeu-sdk;npm uninstall @base44/sdk @base44/vite-plugin. Verify the installed SDK exports what you need before writing code against it:
node --input-type=module -e "import('threeu-sdk/theme').then(t=>console.log(['useProduct','useLead','useAddToCart','useDeliveryMethods','usePaymentMethods','useCoupon','useTracker','TPrice','TAddress'].map(n=>(t[n]?'OK ':'MISS ')+n).join('\n')))"Phase 3 — Shell Checklist
- Add
threeu.jsonwith a full `manifest` —type/name/slug/version/visibility/pages/locales/default_locale/editable_fields/capabilities([Project Setup](/docs/cli-create)). Always declarelocalesand ≥ 3pages. - Re-declare the
@→srcalias invite.config(the base44 plugin used to provide it) and setbase: './',build.outDir: 'preview-dist'. - Replace the base44 client bootstrap with
<ThreeuThemeProvider brand publicToken locale currency analytics>. - Add preview/demo mode: a demo brand + a duck-typed demo storefront client exposing the methods the hooks call, passed as
<ThemeProvider brand={demoBrand} storefront={demoStorefront} preview>when there is no API ([Dev & Publish](/docs/cli-dev)). Without it the theme does not render in review. - Add the theme entry:
defineTheme({ name, slug, version, visibility, pages }). - Make it mergeable: provider-free named
Appexport fromsrc/index.jsx, compiledsrc/styles.cssvia a postbuild step ([Publishing → Mergeability](/docs/themes-publishing)).
Phase 4 — Refactor Page by Page
For each page in the plan, swap the data layer and keep the markup:
- Reads → hooks (
useProducts,useProduct({slug})for variants/options,useCollections, content hooks). - Writes → mutations (
useLead,useCart/useAddToCart,useCheckoutwithuseDeliveryMethods/useDeliveryQuote/usePaymentMethods/useCoupon/<TAddress>,useBooking). - Hero copy / images / colors →
T*components (TText,THeading,TImage,TColor,TSection) with unique, descriptive names — this is what turns a fixed site into a reusable theme. - JSON-LD via
productJsonLd,organizationJsonLd,faqJsonLd,<TJsonLd>. - Keep the app's own AR/EN system, or bind it to
useLocale(). - Delete base44 auth / account / e-services pages and every admin or back-office route.
- Tick each page in
THEME_CONVERSION.md.
4b — Gap pages. Build each approved missing page inside the source design system: read the model page named in the plan, reuse its layout shell, heading scale, card/glass effects, spacing and animations. No new colors, fonts or component idioms — a reviewer must not be able to tell gap pages from originals. Wire them per the survey (SDK hooks with empty-states and demo data, or T* statics) and register them in the router, nav/footer, defineTheme({ pages }), threeu.json and JSON-LD.
4c — Translation retrofit. Reuse the app's i18n system if one exists; otherwise add a light LanguageProvider + dictionary modules + t(key) and a header toggle, bound to useLocale(). Sweep every user-facing string (JSX text, placeholders, aria-labels, alt, toasts, meta) into the dictionary; use natural professional Arabic, not literal calques. Set dir/lang per language, audit direction-sensitive styles (prefer logical properties or rtl: variants), load an Arabic-capable font. Declare locales: ["ar","en"] only if both actually render.
Known Gotchas (from real conversions)
- `product.raw.category` shape differs: demo data often carries a string, the live API returns an eager-loaded category object. Normalize in one helper (
typeof c === 'string' ? c : c?.name) and use it everywhere. - Editable-field defaults are registered once. The editable-field registry freezes each field's first-registered default, so a bilingual default that depends on the active locale does not update on toggle. Use locale-suffixed field names (
hero.title.en/hero.title.ar) or wrap the component so the name changes with the locale. - No booking-availability API yet.
useBooking()creates the booking; time-slot availability stays client-side — document it as a limitation. - Published SDK can lag the docs. If hooks are
MISSin the check above, pin to a newer tarball and record a follow-up to move back to the registry version. - `useStorefront()` throws without a client — preview mode must supply a demo client, a "no data" branch is not enough.
- Content is optional per brand — every content-engine page needs an empty-state.
Phase 5 — Verify
npm run buildand typecheck green;grep -rn "@base44\|base44Client" srcreturns nothing in shipped code.- The preview build renders offline: home, a product, cart/checkout (if present) and the lead form show demo data with no console errors.
- Every gap page renders, is reachable from nav/footer, and is visually consistent with the originals side by side.
- Both locales: toggle and spot-check every page — no untranslated strings, RTL correct (alignment, icon direction, no overflow), declared
localesmatch what renders. - Bundle hygiene:
npx threeu publish --dry-runthentar tzf <bundle> | grep -iE '\.env|token'prints nothing.
Phase 6 — Publish
Report the submission id and status (uploaded = queued for review). Never call it "live" — it goes through review and integration first ([Publishing Themes](/docs/themes-publishing)).
npm run build # → preview-dist/index.html
TOKEN=$(grep '^THREEU_DEVELOPER_TOKEN=' .env.threeu | cut -d= -f2-)
export THREEU_DEVELOPER_TOKEN="$TOKEN" THREEU_API_URL=https://api.threeu.app
npx threeu publish --dry-run
npx threeu publish
curl -s -H "Authorization: Bearer $TOKEN" https://api.threeu.app/api/developer/submissionsTHEME_CONVERSION.md Template
The state file is the contract for Phases 2–6 and the resume point on any new session. Read it first; update it before stopping.
# <App> → ThreeU theme conversion
## Identity: name / key / industry / colors
## Language status: bilingual | partial | monolingual (which) — retrofit decision
## base44 surface (table): entity | op | file | → SDK mapping
## Pages (table): page | type | base44 entities | plan (convert / content / add (new build) / drop) | done
## Missing pages (gap vs canonical set): page | add / skip | design model page
## Per-surface decisions: surface | dynamic / static / drop | notes
## Design: css entry, tailwind, custom effects, assets dir
## Open questions / no-SDK-equivalent items / limitationsPrompt for an AI Coding Agent
Paste this into your coding agent inside the source app's repo (Claude Code, Cursor, Codex, …). It has everything it needs to fetch:
Convert this app into a publishable ThreeU storefront theme.
Read first, in this order:
1. https://docs.threeu.app/docs/themes-convert (the runbook — follow its phases exactly)
2. https://docs.threeu.app/llms-full.txt (full SDK + theme + publishing reference)
Rules: preserve the visual design pixel-for-pixel; replace every base44 (or other
non-ThreeU) data call with threeu-sdk; survey me (dynamic vs static per surface,
missing pages, languages) before changing code; keep state in THEME_CONVERSION.md;
never commit or print my developer token.
My developer token: <paste> — validate it against https://api.threeu.app/api/developer/me
and store it only in a git-ignored .env.threeu.