A hub for Runflow templates and demos. Each folder under projects/ is an independent template — plain HTML, React, Vue, Nuxt, Next.js, whatever — and they all build and deploy together behind a single origin, each served at its own path.
/ → auto-generated index of all templates
/example-html → projects/example-html/ (plain HTML)
/example-next → projects/example-next/ (Next.js static export)
# Install hub dependencies (esbuild, used by the build orchestrator)
npm install
# Build everything into .vercel/output/
npm run build
# Build + serve locally at http://localhost:3000
npm run dev- Create a folder in
projects/with your project files. - If it needs a build step (React, Vue, Next, Nuxt, etc.), include a
package.json. - Run
npm run buildfrom the root — it auto-detects the type and builds everything.
That's it. The folder name becomes the URL path (projects/my-thing/ → /my-thing/).
| Type | How it's detected | Build action |
|---|---|---|
static |
No package.json, or no build script |
Files copied as-is |
vite |
vite in dependencies |
vite build --base /<name>/ |
next |
next in dependencies |
next build (static export) |
nuxt |
nuxt in dependencies |
nuxi generate (static generation) |
nuxt-server |
nuxt + a server/ directory |
Nuxt with NITRO_PRESET=vercel (server functions) |
custom |
Build script + template.config.json |
npm run build + copy output |
node-functions |
template.config.json with "type": "node-functions" |
esbuild-bundled serverless functions + a static dashboard + cron registration |
The right base path is passed to each build via an env var so assets resolve
under /<name>/:
- next →
NEXT_PUBLIC_BASE_PATH=/<name> - nuxt / nuxt-server →
NUXT_APP_BASE_URL=/<name>/ - vite →
--base /<name>/ - custom →
BASE_PATH=/<name>
For frameworks not auto-detected, add a template.config.json:
{
"type": "custom",
"title": "My Template",
"outputDir": "build"
}The BASE_PATH env var (/<project-name>) is passed during build so you can set
asset prefixes. outputDir is where your build writes (defaults to dist).
For templates that need serverless functions plus a static dashboard (e.g. a
cron job that writes to a DB and a dashboard that reads from it), use
node-functions. The hub:
- Installs your project deps (
npm ci/pnpm/bunbased on the lockfile). - Builds your dashboard via
dashboard.buildCmd, copying its output todist/<name>/. - Bundles each
functions.entries[*]with esbuild into the Vercel Build Output API v3 function format (.func/{index.mjs,.vc-config.json}). - Registers any
crons[]you declare into the deployment's rootconfig.json.
Example template.config.json:
{
"type": "node-functions",
"title": "My Template",
"dashboard": {
"dir": ".",
"buildCmd": "npm run build",
"outputDir": "dist"
},
"functions": {
"entries": ["api/cron/run.mjs", "api/metrics.mjs"],
"memory": 1024,
"maxDuration": 300,
"perEntry": {
"api/metrics.mjs": { "maxDuration": 30, "memory": 512 }
}
},
"crons": [
{ "path": "/api/cron/run", "schedule": "*/5 * * * *" }
]
}Conventions:
- Function entries must be
.mjsor.jsand export a default(req, res) => …handler (Node helpers are enabled —req.headers,res.status(),res.json()work). - Cron
pathis relative to the project; the hub prepends/<project-name>automatically. - The dashboard build receives
BASE_PATH=/<project-name>so it can set the right base URL. dashboard.dir,dashboard.outputDir, and everyfunctions.entries[*]are validated to stay inside the project directory (no..escapes, no symlink tunnels).- Sub-daily cron schedules require a paid host plan on most platforms.
Dynamic + catch-all routing. Single-segment dynamic routes use [id].mjs
(the parent function reads ?subId=). For multi-segment catch-alls (e.g.
forwarding arbitrary upstream paths to an API proxy), set
"functions.perEntry": { "api/proxy.mjs": { "catchAll": true } }. The hub emits
a route mapping /<name>/api/proxy/<arbitrary/multi/segment> to the function
with the remainder in ?subPath=; the handler splits that on /.
For cron-protected endpoints, set CRON_SECRET in your host env. The platform
injects Authorization: Bearer ${CRON_SECRET} on each fire and the handler
should validate it.
To list a template that lives on another origin in the landing index, add it to
externals.json:
[
{ "name": "my-other-app", "url": "https://example.com", "title": "Lives elsewhere" }
]These render as outbound links on the landing page. Default is an empty list.
Designed for Vercel (or any host that understands the Build Output API v3).
- Build command:
npm run build - Output:
.vercel/output/(static assets + functions +config.json) - Clean URLs:
/example-next/aboutresolves to…/about.html - Indexable: no
noindexheaders — pages are public and crawlable
- Import the repo.
- Framework Preset: Other.
- Set
CRON_SECRETin the project's environment variables if you use cron functions. - Push to your default branch — done.
runflow-templates/
├── build.mjs # Build orchestrator (detect → build → assemble)
├── build-node-functions.mjs # esbuild bundling for node-functions projects
├── dev.mjs # Local dev server (build + serve on :3000)
├── dev-node-functions.mjs # Dev-time dispatcher for node-functions
├── vercel.json # Host config
├── externals.json # Externally hosted templates (landing links)
├── package.json
├── docs/
│ └── plans/ # Plan/design docs by stage (todo/progress/done)
├── projects/
│ ├── example-html/ # Static HTML template
│ └── example-next/ # Next.js static-export template
└── .vercel/output/ # Build output (gitignored)