create-surf-app 1.0.0-alpha.9 → 1.0.3
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 +64 -0
- package/dist/templates/default/CLAUDE.md +47 -13
- package/dist/templates/default/backend/package.json +1 -1
- package/dist/templates/default/frontend/package.json +1 -1
- package/dist/templates/default/frontend/src/components/ui/resizable.tsx +7 -7
- package/dist/templates/nextjs/CLAUDE.md +27 -1
- package/dist/templates/nextjs/app/providers.tsx +30 -18
- package/dist/templates/nextjs/db/index.ts +1 -1
- package/dist/templates/nextjs/next.config.ts +3 -0
- package/dist/templates/nextjs/package.json +2 -4
- package/package.json +1 -1
- package/dist/templates/default/package.json +0 -11
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.
|
|
@@ -15,13 +15,14 @@ Use `@surf-ai/sdk/server` in backend routes to talk to Surf data APIs.
|
|
|
15
15
|
|
|
16
16
|
```js
|
|
17
17
|
const { dataApi } = require("@surf-ai/sdk/server");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
|
|
19
|
+
// Typed methods per domain — check `@surf-ai/sdk/server` typings for the
|
|
20
|
+
// available method on each domain and its exact parameter shape. Don't
|
|
21
|
+
// copy parameters from one method to another; shapes differ per endpoint.
|
|
22
|
+
const data = await dataApi.<domain>.<method>({ /* params */ });
|
|
23
|
+
|
|
24
|
+
// Escape hatch for endpoints not yet in the typed API:
|
|
25
|
+
const raw = await dataApi.get("<domain>/<endpoint>", { /* params */ });
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
## Structure
|
|
@@ -48,11 +49,7 @@ backend/db/schema.js - define database tables
|
|
|
48
49
|
| `/api/cron/:id` | DELETE | Delete a cron task |
|
|
49
50
|
| `/api/cron/:id/run` | POST | Manually trigger a cron task |
|
|
50
51
|
|
|
51
|
-
Auto-registered
|
|
52
|
-
| File | Endpoint |
|
|
53
|
-
|------|----------|
|
|
54
|
-
| `routes/btc.js` | `/api/btc` |
|
|
55
|
-
| `routes/portfolio.js` | `/api/portfolio` |
|
|
52
|
+
Auto-registered: any file at `backend/routes/<name>.js` is mounted at `/api/<name>`.
|
|
56
53
|
|
|
57
54
|
## Database
|
|
58
55
|
|
|
@@ -68,7 +65,44 @@ exports.users = pgTable("users", {
|
|
|
68
65
|
```
|
|
69
66
|
|
|
70
67
|
Tables are auto-created on startup and when `schema.js` changes (file watcher).
|
|
71
|
-
|
|
68
|
+
|
|
69
|
+
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`:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const { dbQuery } = require("@surf-ai/sdk/db");
|
|
73
|
+
|
|
74
|
+
router.get("/", async (req, res) => {
|
|
75
|
+
const { rows } = await dbQuery(
|
|
76
|
+
"SELECT * FROM users ORDER BY created_at DESC"
|
|
77
|
+
);
|
|
78
|
+
res.json(rows);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
router.post("/", async (req, res) => {
|
|
82
|
+
const { name } = req.body;
|
|
83
|
+
const { rows } = await dbQuery(
|
|
84
|
+
"INSERT INTO users (name) VALUES ($1) RETURNING *",
|
|
85
|
+
[name]
|
|
86
|
+
);
|
|
87
|
+
res.json(rows[0]);
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Environment variables
|
|
92
|
+
|
|
93
|
+
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:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
// backend/routes/config.js
|
|
97
|
+
const express = require("express");
|
|
98
|
+
const router = express.Router();
|
|
99
|
+
|
|
100
|
+
router.get("/", (_req, res) => {
|
|
101
|
+
res.json({ title: process.env.APP_TITLE || "App" });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
module.exports = router;
|
|
105
|
+
```
|
|
72
106
|
|
|
73
107
|
## Do NOT modify
|
|
74
108
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { GripVertical } from "lucide-react"
|
|
2
|
-
import
|
|
2
|
+
import { Group, Panel, Separator } from "react-resizable-panels"
|
|
3
3
|
|
|
4
4
|
import { cn } from "@/lib/utils"
|
|
5
5
|
|
|
6
6
|
const ResizablePanelGroup = ({
|
|
7
7
|
className,
|
|
8
8
|
...props
|
|
9
|
-
}: React.ComponentProps<typeof
|
|
10
|
-
<
|
|
9
|
+
}: React.ComponentProps<typeof Group>) => (
|
|
10
|
+
<Group
|
|
11
11
|
className={cn(
|
|
12
12
|
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
13
13
|
className
|
|
@@ -16,16 +16,16 @@ const ResizablePanelGroup = ({
|
|
|
16
16
|
/>
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
const ResizablePanel =
|
|
19
|
+
const ResizablePanel = Panel
|
|
20
20
|
|
|
21
21
|
const ResizableHandle = ({
|
|
22
22
|
withHandle,
|
|
23
23
|
className,
|
|
24
24
|
...props
|
|
25
|
-
}: React.ComponentProps<typeof
|
|
25
|
+
}: React.ComponentProps<typeof Separator> & {
|
|
26
26
|
withHandle?: boolean
|
|
27
27
|
}) => (
|
|
28
|
-
<
|
|
28
|
+
<Separator
|
|
29
29
|
className={cn(
|
|
30
30
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
31
31
|
className
|
|
@@ -37,7 +37,7 @@ const ResizableHandle = ({
|
|
|
37
37
|
<GripVertical className="h-2.5 w-2.5" />
|
|
38
38
|
</div>
|
|
39
39
|
)}
|
|
40
|
-
</
|
|
40
|
+
</Separator>
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
|
@@ -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
|
|
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 {
|
|
4
|
-
import { useState } from "react"
|
|
3
|
+
import { useEffect } from "react"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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'
|
|
@@ -3,6 +3,9 @@ import type { NextConfig } from 'next'
|
|
|
3
3
|
const nextConfig: NextConfig = {
|
|
4
4
|
output: 'standalone',
|
|
5
5
|
basePath: process.env.BASE_PATH!.replace(/\/+$/, ''),
|
|
6
|
+
env: {
|
|
7
|
+
NEXT_PUBLIC_BASE_PATH: process.env.BASE_PATH!.replace(/\/+$/, ''),
|
|
8
|
+
},
|
|
6
9
|
serverExternalPackages: ['@surf-ai/sdk', 'drizzle-orm', 'drizzle-kit', 'croner'],
|
|
7
10
|
}
|
|
8
11
|
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"type-check": "tsc --noEmit --incremental"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@surf-ai/sdk": "1.0.
|
|
11
|
+
"@surf-ai/sdk": "1.0.3",
|
|
12
12
|
"@surf-ai/theme": "latest",
|
|
13
13
|
"next": "15.5.14",
|
|
14
14
|
"react": "19.2.4",
|
|
15
15
|
"react-dom": "19.2.4",
|
|
16
|
-
"drizzle-orm": "0.
|
|
16
|
+
"drizzle-orm": "0.45.2",
|
|
17
17
|
"drizzle-kit": "0.31.1",
|
|
18
18
|
"croner": "9.1.0",
|
|
19
19
|
"class-variance-authority": "0.7.1",
|
|
@@ -59,8 +59,6 @@
|
|
|
59
59
|
"@hookform/resolvers": "5.2.2",
|
|
60
60
|
"date-fns": "4.1.0",
|
|
61
61
|
"zod": "3.25.76",
|
|
62
|
-
"@tanstack/react-query": "5.94.5",
|
|
63
|
-
"@tanstack/query-core": "5.94.5",
|
|
64
62
|
"scheduler": "0.27.0"
|
|
65
63
|
},
|
|
66
64
|
"devDependencies": {
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "surf-app",
|
|
3
|
-
"private": true,
|
|
4
|
-
"workspaces": ["backend", "frontend"],
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "concurrently \"npm run dev --workspace backend\" \"npm run dev --workspace frontend\""
|
|
7
|
-
},
|
|
8
|
-
"devDependencies": {
|
|
9
|
-
"concurrently": "^9.0.0"
|
|
10
|
-
}
|
|
11
|
-
}
|