Use this file when integrating Figma designs via MCP or making any UI changes.
- Framework: Next.js 15 (App Router) in
apps/web - Styling: Tailwind CSS v3 — utility-first, no CSS Modules, no styled-components
- Components: Custom components in
apps/web/src/components/ - Icons:
lucide-reactexclusively — no other icon libraries - Animations: Tailwind
animate-*+ CSS keyframes inglobals.css - State: React hooks +
@tanstack/react-queryfor server state - Web3:
wagmiv2 +viemfor wallet interactions
All tokens are defined in apps/web/tailwind.config.ts.
// Brand (indigo-blue)
brand-50 → #f0f4ff
brand-400 → #7488ff ← primary interactive / links
brand-500 → #4f5ff7 ← primary buttons / accent
brand-600 → #3a3eec
// Dark surfaces
surface-0 → #0a0a0f ← page background
surface-1 → #111118 ← sidebar, cards
surface-2 → #17171f ← elevated cards
surface-3 → #1e1e28 ← inputs, hover states
surface-4 → #252531 ← active statesfont-sans → Inter var (variable font)
font-mono → JetBrains Mono, Fira CodeFollow Tailwind defaults. Prefer rounded-lg (8px) for cards, rounded-xl (12px) for larger containers.
Defined in apps/web/src/app/globals.css:
| Class | Usage |
|---|---|
.glass |
Frosted dark card — bg-white/5 border border-white/10 backdrop-blur-sm |
.gradient-text |
Brand gradient text — for hero headlines |
.terminal |
Dark monospace code block — green text on surface-1 |
.input |
Standard form input — dark, focused with brand ring |
.deploy-status-dot |
8px status indicator dot |
apps/web/src/
app/ ← Next.js App Router pages
layout.tsx ← root layout (Providers)
page.tsx ← landing page
login/ ← public auth pages
signup/
app/ ← authenticated app shell
layout.tsx ← sidebar + header layout
page.tsx ← dashboard
projects/ ← project management
credits/
settings/
api/ ← API route handlers
components/ ← shared React components
- Files:
kebab-case.tsx - Components:
PascalCase - Pages: default export, named
XxxPage - Server components by default — add
'use client'only when needed
// Server component (default)
export function MyCard({ title }: { title: string }) {
return (
<div className="glass rounded-xl p-6">
<h2 className="font-semibold">{title}</h2>
</div>
);
}
// Client component (stateful)
'use client';
import { useState } from 'react';
export function MyToggle() { ... }When translating Figma designs:
| Figma element | Code equivalent |
|---|---|
| Card / frame | <div className="glass rounded-xl p-6"> |
| Primary button | bg-brand-500 hover:bg-brand-400 text-white rounded-lg |
| Secondary button | bg-white/5 hover:bg-white/10 border border-white/10 |
| Text input | className="input" |
| Status badge (green) | bg-green-500/10 border-green-500/20 text-green-400 |
| Status badge (red) | bg-red-500/10 border-red-500/20 text-red-400 |
| Status badge (yellow) | bg-yellow-500/10 border-yellow-500/20 text-yellow-400 |
| Code / monospace | className="terminal" or font-mono text-xs |
| Muted label | text-white/40 text-xs |
| Divider | border-t border-white/5 |
| Icon | Import from lucide-react, w-4 h-4 default size |
// Always return ApiResult<T> shape
return NextResponse.json({ data: { ... } }); // success
return NextResponse.json({ error: { message: '...' } }, { status: 4xx }); // errorconst token = req.cookies.get('xops_session')?.value
?? req.headers.get('Authorization')?.replace('Bearer ', '');
const session = await validateSession(token);
if (!session) return NextResponse.json({ error: { message: 'Unauthorized' } }, { status: 401 });Package: @decoperations/s3worm (workspace build in this repo; GPR optional for upstream builds)
Singleton access (server-side only):
import { getWorm } from '@/lib/worm'; // apps/web
import { getWorm } from './worm.js'; // apps/builder or packages/build-runnerKey API methods:
worm.save(key, jsonObject); // JSON document
worm.read(key); // read JSON
worm.putBytes(key, data, contentType, opts?); // binary + optional progress
worm.listWithMetadata(prefix); // { key, size, contentType, lastModifiedIso }[]
worm.headObject(key); // metadata
worm.getText(key); // raw UTF-8 text
worm.getPublicUrl(key); // path-style URL when S3_ENDPOINT is set
worm.enableSync({ strategy: 'lww', actor: instanceId }); // hook for oplog-backed buildsPath conventions — always use wormPaths.* from @/lib/worm-paths (web) or ./worm-paths.js (builder / build-runner). Never build key strings ad hoc.
Namespace structure:
users/{userId}/
profile.json
credits.json
projects/{slug}/
project.json
settings.json
deployments/{deployId}/
manifest.json
build.json
logs.txt
ipfs.json
artifacts/
Immutable manifests: save() applies IfNoneMatch: '*' when the key ends with manifest.json (first-write policy for deployment records).
Schema codegen: pnpm --filter @xops/web run worm:codegen (or make worm-codegen) regenerates apps/web/src/generated/worm/models.ts from .worm/schema.json.
Packages that import drizzle-orm query helpers (eq, and, sql, …) in source must list drizzle-orm in that package’s own package.json (even if they already depend on @xops/db), so pnpm + tsc --noEmit resolve the module. Do not remove those entries without also removing the imports or re-exporting helpers from @xops/db.
User id, email, name, avatarUrl
Wallet userId, address, chain, verifiedAt
GitHubAccount userId, githubUserId, username
Project userId, name, slug, repoUrl, framework, buildCommand, outputDir
Deployment projectId, status, cid, publicUrl, manifestPath, costCredits
CreditLedger userId, delta, reason, relatedDeploymentId
Deployment status machine: draft → queued → building → publishing → ready | failed
xops login # device auth flow
xops init # create xops.json
xops link # link to remote project
xops deploy # deploy current directory
xops logs # tail deployment logs
xops projects # list projects
xops whoami # show current userSee .env.example. Minimum to run locally:
DATABASE_URL— postgres connection stringNEXTAUTH_SECRET— random 32-byte secretGITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET— from GitHub OAuth appS3_*— MinIO (local) or AWS S3 credentials
make bootstrap # first-run setup (install + infra + migrations)
make dev # start everything
make infra-up # start Postgres + Redis + MinIO
make db-migrate # run Drizzle migrations
make cli-link # build + link xops CLI globally
make build # production build all packages