Wirely is an AI web UI ideation and editing workspace built with Next.js App Router.
Create a project, pick enabled models/providers, generate one or many page outputs, then iterate HTML in a canvas-style editor with safe previewing and persisted history.
- Multi-mode generation:
single_pageconcept_variantsinformation_architecture
- Multi-output planning for coordinated page sets (up to 3 outputs per run).
- BYOK provider settings per user (Google, OpenRouter, Z.ai, Unsplash).
- Encrypted user API key storage (AES-256-GCM, user-bound AAD, key versioning).
- Quality gating and repair pass for generated HTML.
- Stock image slot planning plus Unsplash resolution and metadata injection.
- Canvas editor with persistent project/page/conversation history in Postgres.
- Safe iframe preview constraints for generated content.
- Route/runtime guardrails:
- rate limits
- request body size limits
- structured logging with sensitive-field redaction
- Next.js 16 (App Router) + React 19 + TypeScript (
strict) - Bun scripts/test runner
- Tailwind CSS v4 + Radix/shadcn-style components
- Zustand editor state store
- Drizzle ORM + Neon Postgres
- AI SDK providers:
- Google (
@ai-sdk/google) - OpenRouter (
@openrouter/ai-sdk-provider) - Z.ai (OpenAI-compatible client via
@ai-sdk/openai)
- Google (
- Clerk authentication (Google OAuth flow)
- Home (
/) loads user session, project history, and enabled model settings. - Settings (
/setting/*) manages profile, provider keys, and enabled models. - Editor (
/wire/[id]) loads pages + conversation history. - Generation requests hit:
POST /api/projects/[projectId]/generate(authenticated proxy)POST /api/wire/[id](planning/generation/repair pipeline)
- Outputs and messages persist to Postgres (
projects,project_pages,conversation_messages,generation_runs,generation_outputs). - Preview renderer sanitizes output and applies iframe security constraints.
- Bun (latest stable recommended)
- Postgres database (Neon or compatible)
- Clerk app configured for Google OAuth
bun installCreate .env.local from the example:
cp .env.example .env.localGenerate an encryption secret for user API keys:
openssl rand -base64 32Use the generated value for USER_API_KEY_MASTER_SECRET_BASE64.
bun run db:migratedb:migrate targets the DATABASE_URL in .env.local — point it at a local
Postgres for development. Production (Vercel) runs on the database in
PROD_DATABASE_URL; apply schema changes there with:
bun run db:migrate:prodDeploy a schema change and its migration in the same push — code that selects a column the production database does not have yet breaks every signed-in page.
Migration
0013_mighty_juggernaut(or later) is required for the live generation progress, per-screen device frames, and prototype flow features: it addsproject_pages.device_typeand theproject_prototype_flowstable.
bun run devOpen http://localhost:3000.
- Sign in via
/login. - Add provider keys at
/setting/provider. - Enable models at
/setting/model. - Create a project from home and generate outputs.
From .env.example:
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | Postgres connection string. |
USER_API_KEY_MASTER_SECRET_BASE64 |
Yes | 32-byte base64 secret for encrypting BYOK provider keys. |
USER_API_KEY_KEY_VERSION |
Yes | Positive integer key version for new encryptions. |
USER_API_KEY_PREVIOUS_MASTER_SECRET_BASE64 |
Optional | Previous 32-byte base64 secret for key rotation compatibility. |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Yes | Clerk frontend key. |
CLERK_SECRET_KEY |
Yes | Clerk backend key. |
NEXT_PUBLIC_CLERK_SIGN_IN_URL |
Yes | Sign-in route, defaults to /login. |
REQUEST_BODY_MAX_BYTES |
Optional | Max JSON body size (default 65536). |
DB_POOL_MAX |
Optional | DB pool size (default 5). |
DB_POOL_CONNECTION_TIMEOUT_MS |
Optional | DB connection timeout ms (default 3000). |
DB_POOL_IDLE_TIMEOUT_MS |
Optional | DB idle timeout ms (default 10000). |
GOOGLE_GENERATIVE_AI_API_KEY |
Optional | Server key used for project title generation fallback. |
OPENROUTER_API_KEY |
Optional | Present in env example for compatibility; user-level key settings are used for generation. |
Notes:
- Generation requests require user-scoped provider keys saved in settings.
- Unsplash is configured in-app via BYOK (
/setting/provider), not via top-level env var.
| Command | Description |
|---|---|
bun run dev |
Start local Next.js dev server. |
bun run build |
Production build. |
bun run start |
Start production server. |
bun run lint |
Run ESLint. |
bun run test |
Run Bun tests. |
bun run db:generate |
Generate Drizzle migration files. |
bun run db:migrate |
Apply Drizzle migrations to the DATABASE_URL database. |
bun run db:migrate:prod |
Apply pending migrations to production (PROD_DATABASE_URL). |
bun run db:backfill:page-html |
Currently points to a missing script path (see Known Limitations). |
bun run agent |
Start the Wirely local agent (runs opencode on your machine). |
bun run agent:doctor |
Check the local opencode install and print the detected flags. |
GET /api/projects- list current user projects.POST /api/projects- create a project (and initial page).POST /api/projects/[projectId]/generate- authenticated proxy into wire generation route.POST /api/wire/[id]- main planning/generation/repair pipeline.GET /api/profile/ai-settings- read provider-key/model settings flags.PATCH /api/profile/ai-settings- update encrypted provider keys and enabled models.GET /api/projects/[projectId]/pages- list pages. With?since=<ISO time>it returns only pages written after that time, every page id, and the next cursor. The open editor polls it every 2.5s while the tab is visible, so MCP edits show without a reload.POST /api/projects/[projectId]/pages- create page.PATCH /api/projects/[projectId]/pages/[pageId]- update page.DELETE /api/projects/[projectId]/pages/[pageId]- delete page.GET /api/projects/[projectId]/pages/[pageId]/png- render the saved page to a PNG with headless Chromium, for the frame's "Copy image" button. Rate limited per user.
Local agent routes (see "Local agent" below):
POST /api/projects/[projectId]/concepts- queue a screen-concept run for the user's local agent.GET /api/projects/[projectId]/concepts/[jobId]- poll job status.GET /api/profile/agent-tokens- list agent tokens and whether an agent is connected.POST /api/profile/agent-tokens- mint an agent token (plaintext returned once).DELETE /api/profile/agent-tokens- revoke an agent token.GET /api/agent/jobs/next- long-poll claim endpoint, bearer auth only.POST /api/agent/jobs/[jobId]/result- agent reports back, bearer auth only.POST /api/mcp- MCP server (stateless streamable HTTP), bearer auth only; see "MCP server" below.
POST /api/mcp is a stateless MCP endpoint: clients POST JSON-RPC messages and read one JSON
body back — no SSE session, nothing stored between requests, so it runs as an ordinary
serverless function. It authenticates with the same personal bearer tokens as the local agent
(minted in Settings → MCP), and never with a Clerk session.
The client's own model does the designing; Wirely stores the results and shows them on the
canvas. Tools: list_projects, create_project, list_pages, add_page, update_page,
patch_page, get_page, get_page_png, delete_page. patch_page replaces one exact
snippet, so an agent can write a page in chunks while the user watches. Screen HTML passes
through the same sandbox sanitizer as generated pages. Every HTML write returns the
lib/wireQuality.ts score and violation ids for the saved page. get_page_png renders the
stored document with headless Chromium (CHROME_PATH picks the local browser in development,
deployments use @sparticuz/chromium). With lint: true it also reports horizontal overflow,
clipped text, and low contrast text from the same render.
The local agent lets Wirely generate designs through the opencode already installed on the
user's machine, using whatever providers they configured with opencode auth login. A GitHub
Copilot, OpenRouter, or Z.AI coding plan therefore works without Wirely integrating any of them.
Credentials never reach the server. The agent dials out over ordinary HTTPS, so no relay, tunnel, or inbound port is involved. Wirely stores the prompt going out and the model text coming back, and nothing else.
Setup:
- Install opencode (1.18.0 or newer) and sign in with
opencode auth login. - Mint a token in Wirely settings, under MCP.
npm install -g wirely-agent.wirely-agent login <token>, thenwirely-agent.
The agent ships as its own npm package (agent/): one bundled file, no dependencies, plain
node 18 or newer. It needs neither this repo nor bun. Point it at a local Wirely with
WIRELY_URL=http://localhost:3000 npx wirely-agent login <token>; the URL is saved with the
token.
For development, bun run agent -- login <token> and bun run agent run it straight from the
repo. The agent detects which way it was started and prints matching help.
The agent paces its own polling rather than the server holding the request open, because a
serverless request is billed for as long as it stays open: parking for 25s cost a full day of
function time per connected agent per day. It asks for work every 2s during a session, every 8s
between prompts, and every 30s once idle for half an hour, all inside the 90s window
isLocalAgentOnline uses. lib/opencode/pollSchedule.ts holds those rates. An agent that sends no
version header is assumed to predate this and gets a short server-side pause so it cannot hot loop.
Publishing the agent: bun run agent:build, then cd agent && npm publish. The build is
gitignored, and prepublishOnly rebuilds it. Bump version in agent/package.json and
AGENT_VERSION in agent/wirely-agent.ts together; a test asserts they match.
Agent environment variables:
| Variable | Default | Description |
|---|---|---|
WIRELY_URL |
https://wirely.vercel.app |
Wirely base URL the agent polls. |
WIRELY_TOKEN |
unset | Token, overriding the one saved in ~/.config/wirely/agent.json. |
OPENCODE_BIN |
opencode |
Path to the opencode binary. |
WIRELY_MODEL |
unset | Default provider/model when a job does not name one. |
Version handling: the agent probes opencode run --help at startup and builds its argv from the
flags that build actually accepts, rather than from a hardcoded version table. This is what keeps
the 1.18.x line (which takes --variant and --dir) and the 2.0.x line (which uses
provider/model#variant and --standalone) both working. Both lines emit the same
--format json event stream, which is what the adapter parses.
- Rate limiting on
POST /api/wire/[id]:- 5 requests per minute
- 30 requests per hour
- keyed by
userId + IP + route
- JSON body guard rejects oversized payloads with
413. - Structured logger redacts sensitive fields and avoids stack traces in production.
- Security headers are applied globally in
next.config.ts. - Generated HTML is sanitized and constrained before iframe preview.
app/ # Next.js routes, pages, API handlers
components/ # Editor UI, canvas, renderers
lib/ # Core logic (auth, db, generation pipeline, security)
store/ # Zustand stores
hooks/ # React hooks
drizzle/ # SQL migrations and metadata snapshots
test/ # Bun tests
Run quality checks before opening a PR:
bun run lint
bun run testIf you change schema:
bun run db:generate
bun run db:migrate # local database
bun run db:migrate:prod # production database, same push as the codedb:backfill:page-htmlcurrently referencesscripts/backfill-page-html-from-conversation.ts, but that file is not present in this repository snapshot.- Rate limiting uses an in-memory store by default (not shared across instances).
- Fork the repo.
- Create a branch for your change.
- Keep changes scoped and include tests when behavior changes.
- Run lint/tests locally.
- Open a pull request with context, rationale, and validation notes.
No LICENSE file is currently present in this repository.
Add one before distributing or accepting external contributions under a specific open source license.