Terreno
The full-stack TypeScript framework for building and launching apps fast.
Terreno is to React Native + Express what Django is to Python web development: a batteries-included framework where the generic, undifferentiated work is already done so you can focus on what makes your app unique — the business logic.
Vision
Most apps need the same foundational pieces: authentication, user management, CRUD APIs, admin panels, real-time updates, AI integrations, consent flows, onboarding carousels. These features are table stakes — they don't differentiate your product, but they take weeks to build from scratch. Terreno handles all of it out of the box.
The goal: all the low-level, generalized code lives in Terreno. Your app only contains business logic.
What Terreno gives you today
- Authentication — Email/password, Google, GitHub, and Apple OAuth with JWT or Better Auth. Login, signup, token refresh, and session management all built in.
- REST APIs in minutes — Define a Mongoose model, pass it to
modelRouter, and get a full CRUD API with permissions, pagination, filtering, sorting, and OpenAPI docs. - 90+ UI components — A themed React Native component library that works on iOS, Android, and web. Forms, tables, modals, navigation — everything you need to build real screens.
- Auto-generated frontend SDK — Your backend's OpenAPI spec generates type-safe RTK Query hooks. Change a backend route and regenerate — no manual API wiring.
- Admin panel — Register your models and get a full admin interface with list views, forms, and reference linking. No custom admin code needed.
- AI integration — Provider-agnostic AI service with streaming chat, text generation, conversation history, and request logging. Plug in any model via Vercel AI SDK.
- Real-time — Socket.io integration with auth-aware connections, auto-reconnect, and token refresh.
Where Terreno is headed
- Onboarding & signup flows — Pre-built carousel-based signup experiences that you configure, not code.
- Consent & legal — Terms of service, privacy policy, and consent form management baked into the framework.
- Notifications — Push notifications, in-app notifications, and email with a unified API.
- File uploads & media — Managed file storage with image processing and CDN integration.
- Feature flags & remote config — Runtime configuration without redeploying.
- Background jobs — Queued task processing for emails, data sync, and scheduled work.
The best way to build with AI
Terreno is designed to be the best framework for AI-assisted app development. The MCP server gives AI coding assistants deep knowledge of Terreno's conventions, enabling them to generate models, routes, screens, and full CRUD features that follow the framework's patterns exactly. The terreno_bootstrap_app tool can scaffold a complete, launchable app from a description — not a toy demo, but a real app with auth, data models, and screens ready to ship.
Philosophy
- Flexible but opinionated. Terreno makes strong default choices (Mongoose, RTK Query, Expo Router) so you don't have to. But every layer is configurable when you need it to be.
- Your app is just business logic. If most apps need it and it doesn't add unique value to your product, it belongs in Terreno, not in your codebase.
- Full-stack coherence. Backend models flow into OpenAPI specs, which generate frontend hooks, which power typed UI components. One change propagates cleanly across the stack.
- Ship, don't configure. Terreno optimizes for getting to a launchable product with minimal effort — not for maximum flexibility at the cost of productivity.
Packages
Published Packages
- api/ - REST API framework built on Express/Mongoose (published as
@terreno/api) - ui/ - React Native UI component library (published as
@terreno/ui) - rtk/ - Redux Toolkit Query utilities for @terreno/api backends (published as
@terreno/rtk) - ai/ - AI service layer with streaming chat, text generation, and Langfuse integration (published as
@terreno/ai) - admin-backend/ - Admin panel backend plugin for @terreno/api (published as
@terreno/admin-backend) - admin-frontend/ - Admin panel frontend screens for @terreno/api backends (published as
@terreno/admin-frontend) - api-health/ - Health check plugin for @terreno/api (published as
@terreno/api-health) - feature-flags/ - Feature flags and A/B testing plugin for @terreno/api (published as
@terreno/feature-flags)
Deployed Services
- mcp-server/ - MCP (Model Context Protocol) server for AI coding assistants (deployed to
mcp.terreno.flourish.health) - example-backend/ - Example backend API (deployed to prod---terreno-backend-example-7knxlrnpqq-uc.a.run.app)
- example-frontend/ - Example frontend app (deployed to terreno-frontend.netlify.app)
Example/Demo Apps
- example-backend/ - Example backend application using
@terreno/api - example-frontend/ - Example frontend application using
@terreno/uiand@terreno/rtk - demo/ - Demo app for showcasing and testing UI components
Feature flags: OpenFeature migration
Terreno’s feature-flag stack uses OpenFeature (@terreno/feature-flags on the server, @terreno/rtk on the client). If you upgrade from a Terreno release that only documented GET …/evaluate, use this checklist.
Backend (@terreno/feature-flags)
- Keep
FeatureFlagsAppregistered as before; admin CRUD is unchanged. - Prefer authenticated clients calling
GET {basePath}/flagConfiguration— the JSON body matches OpenFeature’s static configuration shape (forTypedInMemoryProvideron the client). GET {basePath}/evaluatestill returns the legacyRecord<key, boolean | string | null>map but is deprecated (Deprecation: true,Sunsetheader). Migrate direct HTTP or hand-written clients to/flagConfiguration, or rely on@terreno/rtkhooks below.FeatureFlagdocuments gain optionaldefaultVariant(boolean:"on"/"off"; variant: a key fromvariants). Omitted values are filled on save. For existing databases, run a one-off backfill or reuse the pattern inexample-backend/src/scripts/seed-feature-flags.ts.- Live updates (optional): pass
liveUpdates: { socketIoServer: io | () => io }toFeatureFlagsApp. MongoDB must run as a replica set (a single-node replset is enough) so Mongoose change streams work.
Frontend (@terreno/rtk + OpenFeature)
useFeatureFlags(terrenoApi, …)— no code changes required for the common case; it already uses/flagConfigurationand keeps{ flags, getFlag, getVariant, isLoading, refetch }.- OpenFeature React hooks (
useBooleanFlagValue,useStringFlagValue,<FeatureFlag>, …): add@openfeature/react-sdkand@openfeature/web-sdkto your app (align versions with@terreno/rtkpeerDependencies and the rootcatalogin this repo). - Wrap the UI that reads flags in
<OpenFeatureProvider domain="feature-flags">(or the samedomainyou pass into the hook). - After auth, call
useTerrenoFeatureFlags(terrenoApi, { userId, skip, socket, … })once under your ReduxProviderso OpenFeature is populated before hooks read flag values. - Regenerate the OpenAPI SDK after the backend exposes the new route:
cd your-frontend && bun run sdk. PreferuseTerrenoFeatureFlags/useFeatureFlagsover calling a generatedflagConfigurationhook directly, so domain, cache keys, and optional socket refetch stay consistent.
Further reading
docs/reference/feature-flags.mddocs/how-to/add-feature-flags.mddocs/implementationPlans/feature-flags-openfeature.md
Development
This project uses Bun as the package manager.
Bootstrap
The fastest way to get a development-ready checkout is the top-level bootstrap command. It installs all dependencies and compiles every package so the workspace is ready for development:
bun run bootstrap # Install dependencies + compile all packages
bun run bootstrap:update # Re-run after pulling changes or switching branches
bootstrap— Run when first cloning the repo or creating a new dev environment. Installs all dependencies and compiles every package.bootstrap:update— Run when resuming work after pulling changes, switching branches, or when dependencies have changed. Reinstalls dependencies and recompiles to pick up any changes.
If you prefer to run the steps individually:
# Install dependencies
bun install
Root Scripts
Run commands across all packages or target specific ones:
# All packages
bun run compile # Compile all packages
bun run lint # Lint all packages
bun run lint:fix # Fix lint issues in all packages
bun run test # Run tests in api and ui
# API package (@terreno/api)
bun run api:compile
bun run api:lint
bun run api:test
# UI package (@terreno/ui)
bun run ui:compile
bun run ui:dev # Watch mode
bun run ui:lint
bun run ui:test
# Demo app
bun run demo:compile
bun run demo:lint
bun run demo:start # Start dev server
Note: Make sure to run bun run ui:compile or bun run ui:dev when running the demo app to ensure you have the latest changes in ui while testing or in development
You can also use Bun's filter syntax directly:
bun run --filter '@terreno/ui' compile
bun run --filter '@terreno/api' test
Dependency Management
This monorepo uses Bun Catalogs to manage shared dependency versions across workspaces.
Shared dependency versions are defined in the root package.json under the catalog field:
{
"catalog": {
"react": "19.1.0",
"react-native": "0.81.5",
"typescript": "~5.8.3"
}
}
Workspace packages reference these versions using catalog::
{
"dependencies": {
"react": "catalog:",
"react-native": "catalog:"
}
}
This ensures consistent versions across all packages. To update a shared dependency version, change it in the root catalog and run bun install.
Linking Terreno Packages in Another Repo
Consumers (e.g. Flourish) can develop against local copies of any published package—@terreno/api, @terreno/ui, @terreno/rtk—or multiple at once, using Bun’s link feature.
Which package goes where
- @terreno/api — Link in the consumer’s backend (e.g.
backend/package.json). Restart the server after changes; runbun run api:compileorbun run api:devin terreno so the consumer uses the built output. - @terreno/ui — Link in the consumer’s frontend app (e.g.
app/package.json). When the app uses Metro/Expo, the consumer’s Metro config must be updated (see step 5 below). Runbun run ui:compileorbun run ui:devin terreno so the consumer usesui/dist/. - @terreno/rtk — Link in the consumer’s frontend app. If the app uses Metro and you link rtk, you may need the same Metro resolution tweaks as for ui so dependencies resolve from the app’s
node_modules.
One-time setup in the consumer repo
Clone both repos
Place terreno next to the consumer repo (e.g.flourishandterrenoas siblings). Adjust paths below if your layout differs.Declare the link(s) in the right package.json
In the workspace that depends on the package, set the dependency to the link protocol. Examples for a consumer where terreno is at../terreno:"@terreno/api": "link:../../terreno/api", "@terreno/ui": "link:../../terreno/ui", "@terreno/rtk": "link:../../terreno/rtk"You can link one, two, or all three; use the path that resolves from that package.json to the terreno package directory.
Register and link each package
For each package you’re linking, from the consumer repo:cd ../terreno/<package-dir> && bun link && cd - && cd <consumer-dir> && bun link @terreno/<name>Example for ui when the consumer app is in
app/:cd ../terreno/ui && bun link && cd - && cd app && bun link @terreno/uiRepeat for api (from backend dir) and rtk (from app dir) as needed. Or use scripts in the consumer repo (e.g.
bun run link:ui,bun run link:api) if they exist.Fix symlinks if resolution fails
If Bun creates a bad relative symlink and the package can’t be resolved, replace it with an absolute path. From the consumer workspace that containsnode_modules:rm node_modules/@terreno/<name> ln -s /absolute/path/to/terreno/<package-dir> node_modules/@terreno/<name>Metro (Expo / React Native)
When linking @terreno/ui (and optionally @terreno/rtk) in an Expo/Metro app, the consumer’s Metro config must:- Add the linked package directory (e.g.
terreno/ui) towatchFolders - Resolve the linked package’s dependencies from the app’s
node_modules(e.g.resolver.nodeModulesPathsand aresolveRequestfallback for bare imports from the linked path) so there’s only one copy of React and all deps resolve.
See a consumer that already does this (e.g. Flourish’s
app/metro.config.js) for a reference.- Add the linked package directory (e.g.
Restart dev servers
After linking or Metro config changes, restart the bundler with a clean cache (e.g.bun start --clear). For backend, restart the API server so it picks up the linked@terreno/api.
In the terreno repo
- Run the relevant compile or dev command for each linked package so the consumer sees changes:
bun run api:compile/api:dev,bun run ui:compile/ui:dev,bun run rtk:compile/rtk:dev.
Reverting to published packages
In the consumer’s package.json, set each linked dependency back to a version (e.g. "@terreno/ui": "0.0.17") and run bun install in that workspace.
Releasing
Packages are published to npm automatically when you create a release on GitHub. All publishable packages (@terreno/api, @terreno/ui, @terreno/rtk) are kept in lockstep with the same version number.
Publishing a Release
- Go to the Releases page on GitHub
- Click "Draft a new release"
- Create a new tag with the version number (e.g.,
1.0.0) - novprefix - Fill in the release title and notes
- Click "Publish release"
The GitHub Action will automatically:
- Compare the tag with the previous tag to detect which packages have changes
- Only publish packages that have actual changes since the last release
- Create a PR to update the
package.jsonversions in the repo - Send a Slack notification with the results
How Change Detection Works
The workflow compares each package directory against the previous tag:
- If
api/has changes since the last tag →@terreno/apiis published - If
ui/has changes since the last tag →@terreno/uiis published - If
rtk/has changes since the last tag →@terreno/rtkis published
If no previous tag exists (first release), all packages are published.
Version Format
- Use semantic versioning:
1.0.0,1.2.3,2.0.0-beta.1 - No
vprefix - just the version number - The version from the release tag is applied to all published packages
Required Secrets
The following secrets must be configured in your GitHub repository:
NPM_PUBLISH_TOKEN- npm access token with publish permissionsSLACK_WEBHOOK- (optional) Slack webhook URL for notifications
GCP Static Site Hosting
The demo and example-frontend apps are deployed to Google Cloud Storage with CDN. PR previews are deployed automatically.
GCP Project
- Project ID:
flourish-terreno - Region:
us-east1
Buckets
| App | Bucket | Backend Bucket (CDN) |
|---|---|---|
| example-frontend | flourish-terreno-terreno-frontend-example |
terreno-frontend-example-backend |
| demo | flourish-terreno-terreno-demo |
terreno-demo-backend |
Initial Setup
Run the setup script to create all GCS and CDN resources:
scripts/setup-gcs-hosting.sh
This creates:
- GCS buckets with public read access
- Static website config with SPA fallback (
index.htmlserved for 404s) - Service account write access (prompts for the SA email)
- CDN backend buckets, URL maps, static IPs, HTTP proxies, and forwarding rules
After running the script, point DNS records to the output IPs. To add HTTPS, follow the instructions printed at the end.
Required Secrets
GCP_SA_KEY- Service account key JSON with permissions for GCS and CDN cache invalidation
Workflows
| Workflow | Trigger | What it does |
|---|---|---|
frontend-example-deploy.yml |
Push to master (example-frontend/ui/rtk changes) | Builds and deploys to production bucket |
frontend-example-deploy.yml |
Pull request | Deploys preview to _previews/pr-{number}/ |
demo-deploy.yml |
Push to master (demo/ui changes) | Builds and deploys to production bucket |
demo-deploy.yml |
Pull request | Deploys preview to _previews/pr-{number}/ |
preview-cleanup.yml |
PR closed | Deletes preview files from both buckets |
MCP Server
Terreno provides an MCP (Model Context Protocol) server that enables AI assistants to interact with your backend API. The server is available at mcp.terreno.flourish.health.
Adding the MCP Server to Claude Code
Add the following to your Claude Code MCP settings file (~/.claude/claude_desktop_config.json or .claude/settings.json in your project):
{
"mcpServers": {
"terreno": {
"type": "sse",
"url": "https://mcp.terreno.flourish.health"
}
}
}
Adding the MCP Server to Claude Desktop
Add the following to your Claude Desktop configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"terreno": {
"type": "sse",
"url": "https://mcp.terreno.flourish.health"
}
}
}
Adding the MCP Server to Cursor
Add the following to your Cursor MCP settings (.cursor/mcp.json in your project or global settings):
{
"mcpServers": {
"terreno": {
"type": "sse",
"url": "https://mcp.terreno.flourish.health"
}
}
}
After adding the configuration, restart your AI assistant to connect to the MCP server.
AI Rules Management
This project uses rulesync to maintain consistent AI assistant rules across multiple tools (Cursor, Windsurf, Claude Code, GitHub Copilot).
How It Works
- Single source of truth: Rules are defined in
.rulesync/rules/as markdown files with YAML frontmatter - Generated files: Running
bun run rulesgenerates tool-specific files:.cursorrules- Cursor AI rules.windsurfrules- Windsurf AI rulesCLAUDE.md- Claude Code instructions.github/copilot-instructions.md- GitHub Copilot instructions
- Per-package rules: Each package has its own rules in addition to root-level rules
Commands
bun run rules # Generate all rule files from source
bun run rules:check # Verify generated files are up to date (used in CI)
Updating Rules
- Edit the source files in
.rulesync/rules/ - Run
bun run rulesto regenerate all tool-specific files - Commit both the source and generated files
The CI workflow (.github/workflows/rulesync-check.yml) ensures generated rules stay in sync with source files.