3U

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/sdk call, base44Client, app-params and base44 auth page — static surfaces keep their markup, they do not keep a lingering fetch.
  • Never commit the developer token. Git-ignored .env.threeu only; 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.md inside the source app so the job is resumable.

Phase 0 — Token

  1. Validate: GET https://api.threeu.app/api/developer/me with Authorization: Bearer <token>200 { kind: "developer", account: {…} }. A 401 means wrong/expired — stop. Sandbox has a separate database; validate against production unless told otherwise.
  2. Save to <app>/.env.threeu as THREEU_DEVELOPER_TOKEN=… and THREEU_API_URL=https://api.threeu.app; add .env.threeu to .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:
Find every base44 call
Shell
grep -rnE "@base44|base44Client|base44\.entities|base44\.integrations|base44\.auth|appParams|app-params" src

Phase 1 — Pages & Gap Analysis

Inventory src/pages/ and the router; classify each page:

ClassExamplesBecomes
Storefronthome, products, product, collections, cart, checkout, services, search, offersTheme pages on the SDK
Contentabout, blog, careers, projects, contact, FAQ, legalSDK content engine or static editable pages
Lead-genquote, contact, request-a-demouseLead()
Droplogin, register, password reset, OAuth consent, account, e-services, any admin/dashboard routeRemoved — 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):

SurfaceQuestionSensible default
ProductsLive catalog via useProducts, or the source's hardcoded list as static design? Detail pages with variants?dynamic
ServicesDynamic (type: 'service') or static pages?dynamic if the brand sells services
Numbers / prices / statsLive, or keep static figures?prices dynamic; hero stats static
Cart & checkoutDoes the store sell online? If yes, add a real cart + checkout; if lead-gen only, route CTAs to a quote formadd when it sells
Delivery / paymentsLive methods + rates at checkout, or n/a?live when checkout exists
Lead / quote / contact formsSubmit via useLead so leads reach the brand, or keep e.g. WhatsApp-only?useLead
ContentSDK content engine, static, or drop?SDK with empty-states
Missing pagesPer gap: add (new build in the source design language) or skip?add commerce-critical; offer content pages
Editable vs fixedWhich hero copy / images / colors become T* editable?hero + primary colors
Languages / brandKeep 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.

base44threeu-sdk
Product.filter({published:true}, sort, limit)useProducts({ limit }) → normalized StorefrontProduct[]
Product.filter({type:'service'})useProducts({ type: 'service' })
Product.get(id) / by sluguseProduct({ id }) / useProduct({ slug })product, localized, options, selectedVariant, quantity, selectOption, setQuantity
category / collection listsuseCollections()
Lead.create(data)useLead().submit({ name, phone, email, message, source, utm, payload }) — phone or email required
booking / appointment createuseBooking().book({ serviceSlug, date, time, form })
add to cartuseCart().addItem({ productId, variantId, quantity, price })
place orderuseCheckout().checkout({ items, shipping, paymentMethod, deliveryMethod, discountCode, totalAmount, currency }) — server recomputes fee + discount
couponuseCoupon().apply(code, subtotal) — rejection is data, not a throw
delivery + live rateuseDeliveryMethods() + useDeliveryQuote().quote({ method, lat, lng, subtotal })
payment methodsusePaymentMethods()
Article/Blog.list()storefront.content.blog() / useBlogPost(slug)
careers / FAQ / comparison / legalcontent.careers(), useFaqs(), content.comparisons(), content.legal()
page meta / JSON-LDcontent.meta(path) + <TJsonLd>
pixels / analyticsuseTracker() — GA4-aligned events + Meta/TikTok/Snap/Google pixels + UTM
brand info / logo / paletteuseBrand()
locale / currencyuseLocale(), useCurrency(), <TMoney>, <TLocalizedText>
address {lat,lng,address}<TAddress> + useAddress()
base44.auth.*, Login/Register/Reset pages, base44Client.js, app-params.jsRemove

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 — each queryKey / 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 patternthreeu-sdk
GET /api/products, /api/menu, /api/servicesuseProducts({ type, collection, search })
GET /api/products/:id or :sluguseProduct({ id }) / useProduct({ slug })
GET /api/categoriesuseCollections()
POST /api/contact, /api/quotes, /api/inquiries, /api/newsletteruseLead().submit(...) (phone or email required)
POST /api/bookings, /api/appointments, /api/reservationsuseBooking().book({ serviceSlug, date, time, form })
local cart state, localStorage cart, /api/cartuseCart() / useAddToCart()
POST /api/orders, /api/checkout, Stripe session creationuseCheckout() with useDeliveryMethods, usePaymentMethods, useCoupon, <TAddress> — the payment gateway is the brand's, configured in ThreeU
/api/posts, /api/blog, /api/faqcontent.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 storageno public storefront upload — drop, or reference the file in a lead payload
/api/auth/*, sessions, admin dashboard routes, /admin pagesDelete — ThreeU owns identity and administration
Replit Secrets / process.env on the serverThe 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 point vite.config root at it).
  • Replace the react-query queryFn fetches 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_TOKEN and 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

  1. npm install threeu-sdk; npm uninstall @base44/sdk @base44/vite-plugin. Verify the installed SDK exports what you need before writing code against it:
Check the SDK surface
Shell
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

  1. Add threeu.json with a full `manifest`type/name/slug/version/visibility/pages/locales/default_locale/editable_fields/capabilities ([Project Setup](/docs/cli-create)). Always declare locales and ≥ 3 pages.
  2. Re-declare the @src alias in vite.config (the base44 plugin used to provide it) and set base: './', build.outDir: 'preview-dist'.
  3. Replace the base44 client bootstrap with <ThreeuThemeProvider brand publicToken locale currency analytics>.
  4. 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.
  5. Add the theme entry: defineTheme({ name, slug, version, visibility, pages }).
  6. Make it mergeable: provider-free named App export from src/index.jsx, compiled src/styles.css via 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, useCheckout with useDeliveryMethods/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 MISS in 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 build and typecheck green; grep -rn "@base44\|base44Client" src returns 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 locales match what renders.
  • Bundle hygiene: npx threeu publish --dry-run then tar 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)).

Publish & verify
Shell
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/submissions

THEME_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.

THEME_CONVERSION.md
markdown
# <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 / limitations

Prompt 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:

Agent prompt
text
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.