Feature Dev Practices
Multi-User by Default
Every feature is multi-user. Multiple users access the same deployed feature simultaneously, each with their own identity (feature token). Never design for a single user.
Rules:
- Per-user state (OAuth tokens, preferences, selections) must be stored per-user — use httpOnly cookies (set by the server, scoped to the user's browser) or dashboard rows keyed by user ID
- Shared env vars / Fusebase secrets are global — they are the same for all users. Never use them for per-user credentials or settings
- In-memory backend variables are shared across all requests from all users — never store per-user data in module-level variables
- When integrating third-party APIs with OAuth, each user must go through their own auth flow and get their own tokens
Ask yourself: "If two users open this feature at the same time, will they interfere with each other?" If yes, the design is wrong.
Project Structure
Features are React/Vite apps in features/:
features/
my-feature/
package.json
vite.config.ts
src/
App.tsx
main.tsx
Use existing features in features/ as reference when building new ones.
Vite Config: Ignore Logs Directory
fusebase dev start writes debug logs to <feature-dir>/logs/. Tell Vite's file watcher to ignore this directory so it doesn't trigger unnecessary reloads:
// vite.config.ts
export default defineConfig({
server: {
watch: {
ignored: ['**/logs/**'],
},
},
// ...rest of config
});
Always include this in every feature's vite.config.ts.
Vite Config: Do NOT add inline css.postcss
Do NOT add a css.postcss block in vite.config.ts. An inline css config — even with an empty plugins array — overrides the external postcss.config.js entirely, silently disabling Tailwind and all other PostCSS plugins.
// ❌ BROKEN — overrides postcss.config.js, Tailwind never runs
export default defineConfig({
css: {
postcss: {
plugins: [],
},
},
});
// ✅ CORRECT — no css.postcss block; Vite picks up postcss.config.js automatically
export default defineConfig({
plugins: [react()],
// ...rest of config
});
PostCSS plugins (@tailwindcss/postcss, autoprefixer) belong in postcss.config.js only.
Backend (Optional)
Features may optionally include a backend/ subfolder for a backend API (REST + WebSockets). Do not add a backend unless the feature genuinely needs backend logic — most features work fine with the Dashboard SDK alone. See skill feature-backend for when and how to add one. The backend is served at /api.
Authentication
Features run as the main window. The platform sets a fbsfeaturetoken cookie automatically.
Startup flow:
- Read feature token on app load: check
fbsfeaturetokencookie first, fall back towindow.FBS_FEATURE_TOKENif the cookie is absent - Render app once token is available (show loading state until then)
- Pass token via
x-app-feature-tokenfor direct SDK / Fusebase proxy calls - For calls to the app's own backend (
/api/*), rely on the same-origin cookie and make backend handlers readx-app-feature-tokenor fallback tofbsfeaturetoken
All features MUST handle token expiration (AppTokenValidationError / 401). See skill handling-authentication-errors for the implementation pattern.
User Details
Fetch current user:
const response = await fetch('https://app-api.{FUSEBASE_HOST}/v4/api/users/me', {
headers: { 'x-app-feature-token': featureToken },
})
const user = response.ok ? await response.json() : null
// authenticated: { id: 4124, email: "testemail@gmail.com" }
// anonymous visitor on a public feature: null
Important for public features:
- A visitor feature token may be valid even when
/users/mereturns 401 - In that case, treat the result as
user: null, not as "session expired" - Show the login/auth form for anonymous visitors
- Only show a "Session Expired" modal for actual
AppTokenValidationErrorflows
See skill handling-authentication-errors for the exact 401 handling rules.
Navigation
Use standard browser navigation (React Router, etc.) since features run as the main window. For routing setup, see skill feature-routing.
UI Framework
Use shadcn/ui. For design and UX guidance (layout, tokens, components, accessibility), see skill app-ui-design.
Building Features
cd features/my-feature
npm run build
devDependencies Missing
If npm run build fails because vite/typescript are not found, npm may be running in production mode (NODE_ENV=production — common in VS Code / Claude Code). Fix:
npm install --include=dev
Typecheck at project root
From the repo root, npm run typecheck runs tsc for each feature (see root package.json). It catches strict TypeScript issues that ESLint does not, including the same failures as tsc inside fusebase deploy’s build. Claude Code Stop hooks run it after lint.
Registering Features
After creating a feature, register it via fusebase feature create from the project root:
fusebase feature create --name <name> --subdomain <subdomain> --path <path> --dev-command <command> --build-command <command> --output-dir <dir>
Execute this command automatically after writing the feature code — do not ask the user to run it manually.
Access Principals
Use --access to control who can access the feature. Principals are comma-separated:
# Public (visitor) access
fusebase feature create --name <name> --access=visitor
fusebase feature update <featureId> --access=visitor
# Org role access (guest, client, member, manager, owner)
fusebase feature update <featureId> --access=orgRole:member
fusebase feature update <featureId> --access=orgRole:member,orgRole:client
# Combine visitor and org roles
fusebase feature update <featureId> --access=visitor,orgRole:member
Permissions
Use --permissions with fusebase feature create when the feature is first registered. Only use fusebase feature update --permissions when changing permissions on an already-registered feature. Use MCP to discover dashboard/view IDs. See skill fusebase-cli for permission format and examples.
Getting Feature URLs
fusebase feature list
Lists all features with their deployed URLs. Use this to get actual URLs — do NOT hardcode or guess them.
Always use the full subdomain URL (read FUSEBASE_APP_HOST from .env, e.g. https://my-feature.{FUSEBASE_APP_HOST}/), never relative paths. Each feature is served from its own subdomain root — see skill feature-routing.
Cross-Feature Navigation
Use standard browser navigation (<a>, window.location) with full feature URLs obtained from fusebase feature list.