Imported from Nebutra/Nebutra-Sailor (
AGENTS.md). Install upstream withnpx skills add Nebutra/Nebutra-Sailor. Copyright stays with the author.
AGENTS.md — Nebutra-Sailor
AI coding agent onboarding guide for Cursor, Claude Code, Codex, Windsurf, and GitHub Copilot.
Keep this file repo-relative. Do not add local absolute paths such as
/Users/...or references to sibling checkout paths. If a workflow needs machine-local context, put it in the agent prompt, not in tracked docs.
Project Overview
Nebutra-Sailor is an enterprise-grade SaaS monorepo for AI-native, multi-tenant platforms.
- Runtime: Node.js >= 22, pnpm 10.32+
- Framework: Next.js 16 (App Router, React 19, Turbopack)
- Styling: Tailwind CSS v4 + CSS variables from
@nebutra/tokens - Language: TypeScript 5.9 (strict mode)
- Linting: Biome (not ESLint)
- Testing: Vitest (unit) + Playwright (E2E)
- Monorepo: Turborepo with pnpm workspaces
- License: FSL-1.1-ALv2 (repository, converts to Apache-2.0 after two years) · MIT (published npm packages)
Repository Structure
apps/ # User-facing apps (Next.js / Storybook / Mintlify)
landing/ # Public marketing site (Next.js 16, next-intl, 7 locales)
web/ # Authenticated SaaS dashboard (Next.js 16, Clerk auth)
storybook/ # Component library documentation (Storybook 8.x)
sailor-docs/ # Product/design documentation app
studio/ # Sanity Studio v5 — CMS for blog/changelog/pages
idp/ # Identity Provider application
backends/ # No-UI backends (split by language à la vercel/vercel)
gateway/ # TypeScript / Hono — BFF, auth, tenancy, rate-limit, routing
python/ # Python / FastAPI — only for ML/LLM batch work (see ADR 2026-05-10)
_shared/ ai/
packages/
ai/ agents, MCP, provider adapters
commerce/ billing, contracts, legal, marketing, metering, waitlist
design/ brand, icons, theme, tokens, UI, design sync
iam/ auth, identity, tenant, permissions, audit, vault, OAuth
integrations/ cache, email, event bus, notifications, queue, search, storage, webhooks
ops/ CLI, create-sailor, presets, Sanity, Supabase, China compliance
platform/ analytics, config, db, errors, gateway-core, health, logger, rate-limit
Agent Operating Contract
- Start from
mainunless the user explicitly asks for a branch. Checkgit status --branch --shortbefore edits and preserve unrelated dirty changes. - Read the nearest
AGENTS.mdbefore changing a package or app. The root file gives global defaults; package-local files own the local contract. - Use TDD for behavior changes: write the failing test, run it red, implement, run it green, then refactor.
- Prefer production-proven libraries and provider SDKs over hand-rolled infrastructure. If a provider adapter is only scaffolded, keep metadata honest instead of marking it production-ready.
- Keep checked-in docs and examples portable. Use repo-relative paths and commands; do not mention sibling local checkouts.
- If copying patterns from Supastarter or other starters, copy the proven product behavior, then localize to Nebutra package boundaries and tests.
- Do not broaden a fix into unrelated formatting churn. Stage only intended files when committing.
- Product intelligence phase (2026-09-08). Closure is rescinded. New
product apps are allowed (labs/wip, README states the honesty layer); new
packages only with a real consumer in the same PR. Keep one canonical
implementation per domain, ship security invariants with failing +
regression cases, and keep the full done-chain. PARA information
architecture stays Exploration until the cartography gate in
research/is met. Canonical text: docs/architecture/2026-09-08-product-intelligence-phase.md.
Quick Start
# Install dependencies
pnpm install
# Generate Prisma client
pnpm db:generate
# Start infrastructure (PostgreSQL, Redis, ClickHouse)
pnpm infra:up # full stack
pnpm infra:lite # lightweight (PostgreSQL only)
# Start development
pnpm dev # all apps
pnpm dev:dashboard # web + api-gateway only
pnpm dev:marketing # landing + studio only
# Run tests
pnpm test # unit tests (Vitest)
pnpm e2e # E2E tests (Playwright)
pnpm test:arch # architecture smoke tests
# Type checking & linting
pnpm typecheck # TypeScript check (turbo)
pnpm lint # Biome linting
pnpm lint:fix # Auto-fix lint issues
# Build
pnpm build # production build (all apps)
Key Conventions
Imports — Always Use the Right Package
// UI components
import { Button, Input, Card } from "@nebutra/ui/primitives";
// Layout wrappers
import { PageHeader, EmptyState, LoadingState, ErrorState } from "@nebutra/ui/layout";
// Icons — Geist icons preferred, Lucide for generic
import { Search, Settings } from "@nebutra/icons";
import { ChevronRight } from "lucide-react";
// Theme switching
import { ThemeProvider, useTheme } from "@nebutra/tokens";
// Utility
import { cn } from "@nebutra/ui/utils";
Styling — Semantic Tokens, Not Raw Values
// Use CSS variables from @nebutra/tokens
<div className="bg-[var(--neutral-1)] text-[var(--neutral-12)] border-[var(--neutral-7)]">
// Brand gradient
<h1 style={{ background: "var(--brand-gradient)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }}>
// Never use inline hex values for brand colors
// Never import JS color tokens from @nebutra/ui/theme
Animations — Always Use AnimateIn
import { AnimateIn, AnimateInGroup } from "@nebutra/ui/components";
// Single element
<AnimateIn preset="emerge"><Component /></AnimateIn>
// Staggered list
<AnimateInGroup stagger="normal">
{items.map(item => <AnimateIn key={item.id} preset="fadeUp"><Card /></AnimateIn>)}
</AnimateInGroup>
// Presets: emerge (default), flow, fade, fadeUp, scale
// Never use raw motion.div with hardcoded values
Component Variants — Use CVA
import { cva, type VariantProps } from "class-variance-authority";
const variants = cva("base-classes", { variants: { size: { sm: "p-4", md: "p-6" } } });
Data Fetching Patterns
Server Components (API Gateway)
// Auto-generated typed client from OpenAPI spec
import { getTypedApi } from "@/lib/api/client";
// Server-side (auto-injects Clerk JWT)
const api = await getTypedApi();
const { data } = await api.GET("/api/v1/resource");
Client Components (TanStack Query)
import { useQuery } from "@tanstack/react-query";
import { browserApiClient } from "@/lib/api/client";
const { data, isLoading } = useQuery({
queryKey: ["resource"],
queryFn: () => browserApiClient.GET("/api/v1/resource"),
});
API Types
Types are auto-generated from the Hono OpenAPI spec:
pnpm --filter @nebutra/gateway generate:spec # Export OpenAPI JSON
pnpm generate:api-types # Generate TypeScript types
Authentication & Authorization
-
Provider: Clerk (via
@clerk/nextjs) -
Auth helpers:
apps/web/src/lib/auth.tsgetAuth()— Get userId, orgId, sessionClaimsrequireAuth()— Server-side auth guard (redirects to /sign-in)requireOrg()— Org check (redirects to /select-org)getTenantContext()— Get tenantId + plan from org metadata
-
RBAC:
apps/web/src/lib/permissions.ts- Roles:
admin,member,viewer - Scopes:
resource:actionformat (e.g.,billing:manage,team:invite) - Use
<PermissionGate require="scope">component for UI gating - Use
hasPermission(role, scope)for programmatic checks
- Roles:
Database
- ORM: Prisma v7 with PostgreSQL adapter
- Schema:
packages/db/prisma/schema.prisma(~1,400 lines) - Schemas:
public,auth - Extensions: pgvector, RLS
- Key models: Organization, User, Subscription, AuditLog, ApiKey, Content, Integration
pnpm db:generate # Generate Prisma client
pnpm db:migrate # Run migrations
pnpm db:push # Push schema changes (dev)
pnpm db:studio # Open Prisma Studio
API Gateway (Hono)
Located at backends/gateway/. Middleware stack (in order):
- Request/trace ID correlation
- CORS (dynamic domain allowlist)
- Compression (gzip/deflate/brotli)
- Security headers (HSTS, X-Frame-Options, etc.)
- Rate limiting (token bucket)
- Idempotency (request deduplication)
- Usage metering
- Audit mutation logging
- API versioning
- Tenant context extraction
Route groups: admin/, ai/, billing/, events/, legal/, webhooks/
Testing
Unit Tests (Vitest)
- Config:
vitest.workspace.ts(workspace-level), per-packagevitest.config.ts - Coverage thresholds: 80% lines/functions, 70% branches
- Run:
pnpm testorpnpm test:coverage
Architecture Tests
- Property-based tests using
fast-check - Validates dependency flow, token usage, no-inline-CSS
- Run:
pnpm test:arch
E2E Tests (Playwright)
- Config:
playwright.config.ts - Tests:
e2e/*.spec.ts(landing, auth, dashboard) - CI: 4-way sharded, Chromium
- Run:
pnpm e2eorpnpm e2e:ui
CI/CD
GitHub Actions workflows live in .github/workflows/. The load-bearing ones:
ci.yml— Primary pipeline (lint → typecheck → build → test → e2e → coverage)deploy-*.yml— per-surface deploys. The product surfaces (deploy-gateway,deploy-origin-ecs,deploy-web-vercel,deploy-auth-*,deploy-sailor-docs) are gated by theirDEPLOY_TARGET_*selector (ADR 2026-06-04); the rest (legacy ECS PM2 fallback, Fly, side-project surfaces) carry no selector. No workflow deploys to Kubernetes or publishes container images:deploy.ymlanddocker-build-push.ymlwere retired on 2026-09-02, sodeploy-origin-ecsisworkflow_dispatch-only until an image publish returns, andinfra/iac/k8s/is experimental, not exercised by CI.golden-e2e.yml— golden-path suite (e2e/golden, closure P1.5) against production builds ofapps/web+apps/landing. Nightly, manual, and main pushes touching the golden surface — not a PR gate. Runs the public spec with no secrets; the session-gated specs needE2E_SESSION_COOKIE_VALUE,E2E_DATABASE_URL,E2E_AUTH_SECRET(header of the workflow has the details).security-scan.yml— CodeQL + dependency scanningchromatic.yml— Storybook visual regressionlighthouse-dashboard.yml— Performance monitoring
Supply-chain policy lives in docs/security/supply-chain-governance.md and is
enforced by pnpm supply-chain:verify.
Change detection: Turborepo --affected + dorny/paths-filter for conditional jobs.
Content Management (Sanity)
- Studio:
apps/studio/(Sanity v5) - Client:
packages/sanity/ - Schema types: Post, Author, Category, Page, SiteSettings
- Queries:
getPosts(),getPostBySlug(),getCategories(),getSiteSettings()
What NOT to Do
// Never import from @primer/react (removed)
import { Box } from "@primer/react";
// Never use inline hex for brand colors
<div style={{ color: "#0033FE" }}>
// Never use raw motion.div with hardcoded values
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
// Never use console.log (use @nebutra/logger)
console.log("debug");
// Never hardcode secrets
const key = "sk-proj-xxxxx";
// Never create components without Storybook stories
Useful Commands
| Command | Purpose |
|---|---|
pnpm dev |
Start all apps in dev mode |
pnpm dev:dashboard |
Dashboard + API only |
pnpm dev:marketing |
Landing page + Studio |
pnpm build |
Production build |
pnpm typecheck |
TypeScript check |
pnpm lint |
Biome lint |
pnpm test |
Run unit tests |
pnpm e2e |
Run E2E tests |
pnpm supply-chain:verify |
Verify pnpm/workflow supply-chain guardrails |
pnpm db:generate |
Generate Prisma client |
pnpm db:studio |
Open Prisma Studio |
pnpm infra:up |
Start Docker infrastructure |
pnpm brand:sync |
Sync brand assets |
pnpm generate:api-types |
Regenerate API types from OpenAPI |
File Naming Conventions
- Components:
kebab-case.tsx(e.g.,auth-banner.tsx) - Tests:
*.test.tsor*.spec.ts - E2E tests:
e2e/*.spec.ts - Storybook:
*.stories.tsx - Config:
*.config.ts - Types: co-located with source, or
types.ts
Package Boundaries
| Need | Package | Import |
|---|---|---|
| UI chrome | @nebutra/ui |
@nebutra/ui/primitives |
| Composed patterns | @nebutra/ui |
@nebutra/ui/components |
| Chat surfaces | @nebutra/ui |
@nebutra/ui/chat |
| Layout shells | @nebutra/ui |
@nebutra/ui/layout |
| CSS variables | @nebutra/tokens |
@import "@nebutra/tokens/styles.css" |
| Icons | @nebutra/icons |
Named exports |
| Database | @nebutra/db |
Prisma client |
| Auth adapter | @nebutra/identity |
Provider adapters |
| Billing | @nebutra/billing |
Stripe operations |
@nebutra/email |
Send templates | |
| AI | @nebutra/agents |
streamText, generateText, embed, BaseAgent, AgentOrchestrator |
| Logging | @nebutra/logger |
Structured logger |
| Errors | @nebutra/errors |
Typed errors |
| Audit | @nebutra/audit |
Log audit events |