create-surf-app 1.0.0-alpha.4 → 1.0.0-alpha.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # create-surf-app
2
+
3
+ Scaffold a Surf app pre-wired with [`@surf-ai/sdk`](../sdk).
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create surf-app@latest [project-name] [--template <vite|nextjs>]
9
+ ```
10
+
11
+ - `project-name` — directory to create (defaults to current directory)
12
+ - `--template` — `vite` (default) or `nextjs`
13
+
14
+ Examples:
15
+
16
+ ```bash
17
+ npm create surf-app@latest my-app
18
+ npm create surf-app@latest my-app --template nextjs
19
+ npm create surf-app@latest . # scaffold into current dir
20
+ ```
21
+
22
+ ## Templates
23
+
24
+ | Template | Stack |
25
+ | --- | --- |
26
+ | `vite` (default) | Vite + React + Express backend using `@surf-ai/sdk/server` |
27
+ | `nextjs` | Next.js App Router with API route handlers using `@surf-ai/sdk/server` |
28
+
29
+ ## Environment variables
30
+
31
+ `SURF_API_KEY` is the only secret you need to provide. Everything else has a sensible default — but `BASE_PATH` must be **defined** (empty string is fine and means root) because the scaffolds read it unconditionally.
32
+
33
+ ### `vite` template
34
+
35
+ `backend/.env`:
36
+
37
+ | Var | Required | Default | Purpose |
38
+ | --- | --- | --- | --- |
39
+ | `SURF_API_KEY` | yes (non-empty) | — | Bearer token for Surf upstream + protected runtime endpoints |
40
+ | `BACKEND_PORT` | no | `3001` | Express server port |
41
+ | `SURF_API_BASE_URL` | no | `https://api.asksurf.ai/gateway/v1` | Override Surf API base URL |
42
+
43
+ `frontend/.env`:
44
+
45
+ | Var | Required | Default | Purpose |
46
+ | --- | --- | --- | --- |
47
+ | `BASE_PATH` | yes (empty OK) | — | Vite base path (e.g. `/preview/abc/`); empty means root |
48
+ | `PORT` | no | `5173` | Vite dev server port |
49
+ | `BACKEND_PORT` | no | `3001` | Backend port the dev server proxies `/api` to |
50
+
51
+ ### `nextjs` template
52
+
53
+ `.env`:
54
+
55
+ | Var | Required | Default | Purpose |
56
+ | --- | --- | --- | --- |
57
+ | `SURF_API_KEY` | yes (non-empty) | — | Bearer token for Surf upstream + protected runtime endpoints |
58
+ | `BASE_PATH` | yes (empty OK) | — | Next.js `basePath` (e.g. `/preview/abc`); empty means root |
59
+ | `PORT` | no | `3000` | Next.js server port |
60
+ | `SURF_API_BASE_URL` | no | `https://api.asksurf.ai/gateway/v1` | Override Surf API base URL |
61
+
62
+ `SURF_API_KEY` is enforced at **dev/start** time, not build time — `npm run build` succeeds without it so CI builds don't need the secret.
63
+
64
+ See [`@surf-ai/sdk` README](../sdk/README.md) for full SDK configuration and runtime details.
@@ -33,14 +33,8 @@ Done! Next steps:
33
33
  Done! Next steps:
34
34
 
35
35
  cd ${name}
36
- cd backend && npm install && cd ..
37
- cd frontend && npm install && cd ..
38
-
39
- # Start backend
40
- cd backend && npm run dev &
41
-
42
- # Start frontend
43
- cd frontend && npm run dev
36
+ npm install
37
+ npm run dev
44
38
 
45
39
  Open the local URL printed by Vite
46
40
  `);
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSurfApp
4
- } from "./chunk-BV3XIPFM.js";
4
+ } from "./chunk-FDATV75D.js";
5
5
 
6
6
  // src/cli.ts
7
7
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createSurfApp
3
- } from "./chunk-BV3XIPFM.js";
3
+ } from "./chunk-FDATV75D.js";
4
4
  export {
5
5
  createSurfApp
6
6
  };
@@ -68,7 +68,44 @@ exports.users = pgTable("users", {
68
68
  ```
69
69
 
70
70
  Tables are auto-created on startup and when `schema.js` changes (file watcher).
71
- The agent can also call `POST /api/__sync-schema` explicitly after editing.
71
+
72
+ Query the database in routes with `dbQuery(sql, params)` from `@surf-ai/sdk/db`. Drizzle ORM is **only** used to declare the schema — there is no Drizzle client, no `req.db` middleware, and no direct connection pool. `dbQuery` returns a pg-style result `{ rows, rowCount, fields }`, so destructure `rows`:
73
+
74
+ ```js
75
+ const { dbQuery } = require("@surf-ai/sdk/db");
76
+
77
+ router.get("/", async (req, res) => {
78
+ const { rows } = await dbQuery(
79
+ "SELECT * FROM users ORDER BY created_at DESC"
80
+ );
81
+ res.json(rows);
82
+ });
83
+
84
+ router.post("/", async (req, res) => {
85
+ const { name } = req.body;
86
+ const { rows } = await dbQuery(
87
+ "INSERT INTO users (name) VALUES ($1) RETURNING *",
88
+ [name]
89
+ );
90
+ res.json(rows[0]);
91
+ });
92
+ ```
93
+
94
+ ### Environment variables
95
+
96
+ Only variables prefixed with `VITE_` are exposed to the Vite frontend (`import.meta.env.VITE_FOO`). To use a plain env var (e.g. `APP_TITLE`) in the UI, read it in a backend route and fetch it from the frontend:
97
+
98
+ ```js
99
+ // backend/routes/config.js
100
+ const express = require("express");
101
+ const router = express.Router();
102
+
103
+ router.get("/", (_req, res) => {
104
+ res.json({ title: process.env.APP_TITLE || "App" });
105
+ });
106
+
107
+ module.exports = router;
108
+ ```
72
109
 
73
110
  ## Do NOT modify
74
111
 
@@ -88,3 +125,23 @@ The agent can also call `POST /api/__sync-schema` explicitly after editing.
88
125
  - Do not bypass your backend routes from the frontend
89
126
  - Frontend packages are pre-installed - check `package.json` before installing
90
127
  - Default to a dark theme unless the user explicitly asks for a different visual direction.
128
+
129
+ ## Design
130
+
131
+ ### Avoid AI-default patterns
132
+ - No colored icon boxes next to metrics (the blue-bg-with-icon KPI pattern)
133
+ - No gradient avatar circles with initials
134
+ - No "Built with React · Tailwind" footers
135
+ - No AI copywriting: "Elevate", "Seamless", "Unleash", "Delve", "Next-Gen"
136
+ - No "Oops!" error messages — be direct ("Failed to load. Try again.")
137
+ - No round placeholder numbers ($100.00) — use realistic data ($847.29)
138
+ - Sentence case on headings, not Title Case On Every Word
139
+ - Icons should aid scanning, not decorate — omit when the label is clear
140
+
141
+ ### ECharts
142
+ - Flat style: show primary axis line, dashed split lines, transparent chart bg
143
+ - Custom tooltip formatter with dash indicators (12×2.5px bars, not default circle dots)
144
+ - Legend: type "plain", icon "roundRect", itemWidth 12, itemHeight 3
145
+ - Prefer timeframe tabs (7D/30D/90D/1Y/All) over dataZoom for time series
146
+ - Default to theme visualizer palette; override when semantics demand it (red/green for gain/loss, sequential scales for heatmaps)
147
+ - Dark mode: parameterize tooltip bg, axis colors, split line colors via resolvedTheme
@@ -2,11 +2,10 @@
2
2
  "name": "backend",
3
3
  "private": true,
4
4
  "scripts": {
5
- "start": "node scripts/check-env.js node server.js",
6
5
  "dev": "node scripts/check-env.js node --watch server.js"
7
6
  },
8
7
  "dependencies": {
9
- "@surf-ai/sdk": "1.0.0-alpha.4",
8
+ "@surf-ai/sdk": "1.0.0-alpha.40",
10
9
  "express": "4.22.1"
11
10
  }
12
11
  }
@@ -1,3 +1,3 @@
1
- FRONTEND_PORT=5173
1
+ PORT=5173
2
2
  BACKEND_PORT=3001
3
3
  BASE_PATH=
@@ -78,6 +78,6 @@
78
78
  "tw-animate-css": "1.4.0",
79
79
  "typescript-eslint": "8.57.1",
80
80
  "typescript": "5.9.3",
81
- "vite": "6.4.1"
81
+ "vite": "6.4.2"
82
82
  }
83
83
  }
@@ -3,7 +3,7 @@
3
3
  * Loads .env manually, checks vars, then execs the actual command.
4
4
  *
5
5
  * Build: BASE_PATH (defined, can be empty), BACKEND_PORT
6
- * Dev: FRONTEND_PORT, BACKEND_PORT, BASE_PATH
6
+ * Dev: PORT, BACKEND_PORT, BASE_PATH
7
7
  */
8
8
  const fs = require('node:fs')
9
9
  const path = require('node:path')
@@ -29,7 +29,7 @@ const isBuild = args.some(a => a.includes('build'))
29
29
  // Vars that must be non-empty
30
30
  const requiredNonEmpty = isBuild
31
31
  ? ['BACKEND_PORT']
32
- : ['FRONTEND_PORT', 'BACKEND_PORT']
32
+ : ['PORT', 'BACKEND_PORT']
33
33
 
34
34
  // Vars that must be defined (empty is ok — BASE_PATH="" means root)
35
35
  const requiredDefined = ['BASE_PATH']
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react'
4
4
  import tailwindcss from '@tailwindcss/vite'
5
5
 
6
6
  export default defineConfig(() => {
7
- const frontendPort = Number.parseInt(process.env.FRONTEND_PORT || '', 10)
7
+ const frontendPort = Number.parseInt(process.env.PORT || '', 10)
8
8
  const backendPort = Number.parseInt(process.env.BACKEND_PORT || '', 10)
9
9
  const base = process.env.BASE_PATH || './'
10
10
  const hasAbsBase = base.startsWith('/')
@@ -1,3 +1,3 @@
1
- FRONTEND_PORT=3000
1
+ PORT=3000
2
2
  BASE_PATH=
3
3
  SURF_API_KEY=
@@ -72,6 +72,32 @@ export const users = pgTable("users", {
72
72
 
73
73
  Tables are auto-synced on server start and when `db/schema.ts` changes in dev mode.
74
74
 
75
+ Query the database in API routes using the `db()` helper re-exported from `@/db`. Drizzle ORM is **only** used to declare the schema — there is no Drizzle client and no direct connection pool. `db(sql, params)` returns a pg-style result `{ rows, rowCount, fields }`, so destructure `rows`:
76
+
77
+ ```ts
78
+ import { db } from "@/db";
79
+
80
+ export async function GET() {
81
+ const { rows } = await db(
82
+ "SELECT * FROM users ORDER BY created_at DESC"
83
+ );
84
+ return Response.json(rows);
85
+ }
86
+
87
+ export async function POST(request: Request) {
88
+ const { name } = await request.json();
89
+ const { rows } = await db(
90
+ "INSERT INTO users (name) VALUES ($1) RETURNING *",
91
+ [name]
92
+ );
93
+ return Response.json(rows[0]);
94
+ }
95
+ ```
96
+
97
+ ### Environment variables
98
+
99
+ Only variables prefixed with `NEXT_PUBLIC_` are exposed to the browser. To use a plain env var (e.g. `APP_TITLE`) in a client component, either read it in a server component / route handler and pass it down as a prop, or expose it through a backend route.
100
+
75
101
  ## Do NOT modify
76
102
 
77
103
  - `instrumentation.ts` - server boot (schema sync, cron)
@@ -79,7 +105,7 @@ Tables are auto-synced on server start and when `db/schema.ts` changes in dev mo
79
105
  - `db/index.ts` - database connection
80
106
  - `lib/boot.ts` - infrastructure (schema sync, cron init)
81
107
  - `app/layout.tsx` - root layout and providers
82
- - `app/providers.tsx` - client-side providers (QueryClient, theme)
108
+ - `app/providers.tsx` - client-side preview bridge hooks
83
109
  - `eslint.config.mjs` - lint rules
84
110
  - `globals.css` - only imports, do not add styles here (use Tailwind classes)
85
111
 
@@ -1,23 +1,35 @@
1
1
  "use client"
2
2
 
3
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4
- import { useState } from "react"
3
+ import { useEffect } from "react"
5
4
 
6
- export function Providers({ children }: { children: React.ReactNode }) {
7
- const [queryClient] = useState(
8
- () =>
9
- new QueryClient({
10
- defaultOptions: {
11
- queries: {
12
- refetchOnWindowFocus: false,
13
- retry: 3,
14
- staleTime: 30 * 1000,
15
- },
16
- },
17
- })
18
- )
5
+ // Notify parent frame that the app has rendered.
6
+ // DO NOT REMOVE — the hosting app uses this to dismiss the loading overlay.
7
+ function useSurfAppReady() {
8
+ useEffect(() => {
9
+ try {
10
+ window.parent.postMessage({ type: "surf-app-ready" }, "*")
11
+ } catch {
12
+ /* cross-origin — ignore */
13
+ }
14
+ }, [])
15
+ }
19
16
 
20
- return (
21
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
22
- )
17
+ // Patch fetch so `/api/*` calls automatically include basePath.
18
+ // Without this, `fetch('/api/...')` hits the parent app's routes instead of
19
+ // the preview dev server's API routes.
20
+ // DO NOT REMOVE — this is required for the preview proxy architecture.
21
+ const _basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""
22
+ if (typeof window !== "undefined" && _basePath) {
23
+ const _origFetch = window.fetch
24
+ window.fetch = function patchedFetch(this: typeof globalThis, input: RequestInfo | URL, init?: RequestInit) {
25
+ if (typeof input === "string" && input.startsWith("/api/")) {
26
+ input = _basePath + input
27
+ }
28
+ return _origFetch.call(this, input, init)
29
+ } as typeof window.fetch
30
+ }
31
+
32
+ export function Providers({ children }: { children: React.ReactNode }) {
33
+ useSurfAppReady()
34
+ return children
23
35
  }
@@ -3,6 +3,6 @@
3
3
  //
4
4
  // Usage in route handlers:
5
5
  // import { db } from '@/db'
6
- // const result = await db('SELECT * FROM users')
6
+ // const { rows } = await db('SELECT * FROM users')
7
7
 
8
8
  export { dbQuery as db, dbProvision, dbTables, dbTableSchema, dbStatus } from '@surf-ai/sdk/db'
@@ -1,7 +1,11 @@
1
1
  import type { NextConfig } from 'next'
2
2
 
3
3
  const nextConfig: NextConfig = {
4
+ output: 'standalone',
4
5
  basePath: process.env.BASE_PATH!.replace(/\/+$/, ''),
6
+ env: {
7
+ NEXT_PUBLIC_BASE_PATH: process.env.BASE_PATH!.replace(/\/+$/, ''),
8
+ },
5
9
  serverExternalPackages: ['@surf-ai/sdk', 'drizzle-orm', 'drizzle-kit', 'croner'],
6
10
  }
7
11
 
@@ -4,17 +4,16 @@
4
4
  "scripts": {
5
5
  "dev": "node scripts/check-env.js next dev",
6
6
  "build": "node scripts/check-env.js next build",
7
- "start": "node scripts/check-env.js next start",
8
7
  "lint": "eslint .",
9
8
  "type-check": "tsc --noEmit --incremental"
10
9
  },
11
10
  "dependencies": {
12
- "@surf-ai/sdk": "1.0.0-alpha.4",
11
+ "@surf-ai/sdk": "1.0.0-alpha.40",
13
12
  "@surf-ai/theme": "latest",
14
13
  "next": "15.5.14",
15
14
  "react": "19.2.4",
16
15
  "react-dom": "19.2.4",
17
- "drizzle-orm": "0.44.2",
16
+ "drizzle-orm": "0.45.2",
18
17
  "drizzle-kit": "0.31.1",
19
18
  "croner": "9.1.0",
20
19
  "class-variance-authority": "0.7.1",
@@ -60,8 +59,6 @@
60
59
  "@hookform/resolvers": "5.2.2",
61
60
  "date-fns": "4.1.0",
62
61
  "zod": "3.25.76",
63
- "@tanstack/react-query": "5.94.5",
64
- "@tanstack/query-core": "5.94.5",
65
62
  "scheduler": "0.27.0"
66
63
  },
67
64
  "devDependencies": {
@@ -29,7 +29,7 @@ const isBuild = args.includes('build')
29
29
  // Vars that must be non-empty
30
30
  const requiredNonEmpty = isBuild
31
31
  ? []
32
- : ['FRONTEND_PORT', 'SURF_API_KEY']
32
+ : ['PORT', 'SURF_API_KEY']
33
33
 
34
34
  // Vars that must be defined (empty is ok — e.g. BASE_PATH="" means root)
35
35
  const requiredDefined = ['BASE_PATH']
@@ -44,11 +44,6 @@ if (missing.length > 0) {
44
44
  process.exit(1)
45
45
  }
46
46
 
47
- // Pass FRONTEND_PORT as PORT so Next.js uses it
48
- if (process.env.FRONTEND_PORT) {
49
- process.env.PORT = process.env.FRONTEND_PORT
50
- }
51
-
52
47
  // Run the actual command
53
48
  try {
54
49
  execSync(args.join(' '), { stdio: 'inherit', env: process.env })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-surf-app",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.40",
4
4
  "description": "Scaffold a Surf app — Vite + React + Express + @surf-ai/sdk",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +0,0 @@
1
- {
2
- "name": "surf-app",
3
- "private": true,
4
- "scripts": {
5
- "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
6
- "install:all": "cd backend && npm install && cd ../frontend && npm install"
7
- },
8
- "devDependencies": {
9
- "concurrently": "^9.0.0"
10
- }
11
- }