diff --git a/.github/actions/build-and-serve/action.yml b/.github/actions/build-and-serve/action.yml
new file mode 100644
index 0000000..fc10ac5
--- /dev/null
+++ b/.github/actions/build-and-serve/action.yml
@@ -0,0 +1,60 @@
+name: Build and serve an example
+description: >
+ Builds one example with nixpacks, runs it with PORT injected, and waits for a
+ real HTTP response. Shared by both matrices so the two groups are checked
+ identically.
+
+inputs:
+ example:
+ description: Path of the example directory
+ required: true
+ port:
+ description: Port the app listens on
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Install nixpacks
+ shell: bash
+ run: curl -fsSL https://nixpacks.com/install.sh | bash
+
+ - name: Build ${{ inputs.example }}
+ shell: bash
+ run: nixpacks build "${{ inputs.example }}" --name example:ci
+
+ - name: Serve a request
+ shell: bash
+ env:
+ PORT: ${{ inputs.port }}
+ run: |
+ set -euo pipefail
+
+ docker run -d --name example \
+ -e PORT="${PORT}" -p "8099:${PORT}" example:ci
+
+ # Compiled and JVM apps take noticeably longer to become ready than
+ # interpreted ones, so this polls rather than sleeping a fixed time.
+ for attempt in $(seq 1 90); do
+ if curl -sf -o /tmp/body.txt "http://127.0.0.1:8099/"; then
+ echo "responded after ${attempt}s:"
+ head -c 200 /tmp/body.txt
+ exit 0
+ fi
+ # Failing fast on a dead container beats waiting out the full timeout.
+ if [ "$(docker inspect -f '{{.State.Status}}' example)" = "exited" ]; then
+ echo "::error::container exited before serving a request"
+ docker logs example
+ exit 1
+ fi
+ sleep 1
+ done
+
+ echo "::error::no response after 90s"
+ docker logs example
+ exit 1
+
+ - name: Container logs on failure
+ if: failure()
+ shell: bash
+ run: docker logs example || true
diff --git a/.github/workflows/starters.yml b/.github/workflows/starters.yml
new file mode 100644
index 0000000..faf2fab
--- /dev/null
+++ b/.github/workflows/starters.yml
@@ -0,0 +1,137 @@
+name: Language starters
+
+# The minimal per-language starters under examples/starters are advertised as
+# "fork it, connect it to Temps, deploy". That claim is only true if something
+# checks it, so each is built the way Temps builds it — with nixpacks — and
+# then asked to serve a request.
+#
+# A build that succeeds but produces a container which never answers is the
+# failure this catches, and the one that is invisible without it.
+
+on:
+ push:
+ branches: [main]
+ paths: ["examples/starters/**", ".github/workflows/starters.yml", ".github/actions/**"]
+ pull_request:
+ paths: ["examples/starters/**", ".github/workflows/starters.yml", ".github/actions/**"]
+ # Base images and package registries move underneath these examples even
+ # when nobody touches the repository.
+ schedule:
+ - cron: "0 6 * * 1"
+ workflow_dispatch:
+
+concurrency:
+ group: examples-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ # Examples nixpacks can build and run today. Regressions here are real.
+ supported:
+ name: ${{ matrix.example }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { example: examples/starters/nodejs/express, port: 3000 }
+ - { example: examples/starters/nodejs/fastify, port: 3000 }
+ - { example: examples/starters/nodejs/nestjs, port: 3000 }
+ - { example: examples/starters/nextjs/app-router, port: 3000 }
+ - { example: examples/starters/vite/react, port: 3000 }
+ - { example: examples/starters/sveltekit, port: 3000 }
+ - { example: examples/starters/python/flask, port: 8000 }
+ - { example: examples/starters/python/fastapi, port: 8000 }
+ - { example: examples/starters/rust/actix, port: 3000 }
+ - { example: examples/starters/php/vanilla, port: 3000 }
+ - { example: examples/starters/php/laravel, port: 3000 }
+ - { example: examples/starters/elixir/phoenix, port: 4000 }
+ - { example: examples/starters/dockerfile, port: 3000 }
+ steps:
+ - uses: actions/checkout@v4
+ - uses: docker/setup-buildx-action@v3
+ - uses: ./.github/actions/build-and-serve
+ with:
+ example: ${{ matrix.example }}
+ port: ${{ matrix.port }}
+
+ # Starters that are correct but which nixpacks cannot currently build.
+ #
+ # These ran as a 13-job matrix with continue-on-error, which does not stop
+ # GitHub painting each one red — a passing PR looked like a broken one. They
+ # are one always-green job now, reporting to the run summary instead.
+ #
+ # The point is not to hide them. It is to notice the day nixpacks catches up:
+ # any starter that starts building is called out as news at the top of the
+ # summary, because that is the signal worth acting on.
+ nixpacks-gaps:
+ name: Known nixpacks gaps (report only)
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@v4
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Install nixpacks
+ run: curl -fsSL https://nixpacks.com/install.sh | bash
+
+ - name: Probe
+ run: |
+ set -uo pipefail
+
+ # starter|reason it is expected to fail
+ GAPS=(
+ "go/net-http|go.mod declares go 1.24; Nix ships an older Go"
+ "go/gin|go.mod declares go 1.24; Nix ships an older Go"
+ "dotnet/web|Nix provides dotnet-sdk 6.0.413 for a net9.0 project"
+ "astro|Nix npm is older than the required npm >=9.6.5"
+ "nuxt|Nix npm is older than the required npm >=9.6.5"
+ "swift/vapor|swift: command not found"
+ "deno|no build plan for a bare main.ts"
+ "java/spring-boot|no build plan for build.gradle without a wrapper"
+ "bun/bun-server|bun: not found"
+ "bun/elysia|bun: not found"
+ "nodejs/hono|bun: not found"
+ "python/django|container exits at startup"
+ "ruby/rails|build fails under the generated plan"
+ )
+
+ fixed=()
+ still=()
+
+ for entry in "${GAPS[@]}"; do
+ starter="${entry%%|*}"
+ reason="${entry#*|}"
+ if nixpacks build "examples/starters/${starter}" --name gap:ci >/dev/null 2>&1; then
+ fixed+=("${starter}")
+ else
+ still+=("${starter}|${reason}")
+ fi
+ done
+
+ {
+ echo "## Known nixpacks gaps"
+ echo
+ if [ "${#fixed[@]}" -gt 0 ]; then
+ echo "### :tada: These now build — nixpacks has caught up"
+ echo
+ echo "Move them into the \`supported\` matrix and delete them from this list."
+ echo
+ for s in "${fixed[@]}"; do echo "- \`${s}\`"; done
+ echo
+ fi
+ echo "### Still failing (${#still[@]} of ${#GAPS[@]})"
+ echo
+ echo "These are correct applications. They build and serve with a current"
+ echo "toolchain; nixpacks resolves runtimes from the Nix package set, which lags."
+ echo
+ echo "| Starter | Why |"
+ echo "|---|---|"
+ for entry in "${still[@]}"; do
+ echo "| \`${entry%%|*}\` | ${entry#*|} |"
+ done
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ # Always green: a gap that is already known and documented is not a
+ # regression, and a red check here would train people to ignore it.
+ echo "${#fixed[@]} now building, ${#still[@]} still failing — see the run summary"
diff --git a/README.md b/README.md
index 4903d57..db22922 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,21 @@ Production-ready example applications showcasing [Temps](https://temps.sh) SDK i
| [docker/rust-axum](./examples/docker/rust-axum) | Rust Axum, PostgreSQL | Notes/snippets API |
| [docker/java-spring](./examples/docker/java-spring) | Spring Boot 3.5, PostgreSQL, Redis | Contacts API with JPA |
+### Language starters
+
+Minimal one-file-per-language apps — the smallest thing that builds and serves
+a request. See [examples/starters](./examples/starters) for the full list and
+for which ones nixpacks can currently build.
+
+| Starter | Stack |
+|---------|-------|
+| [starters/nodejs](./examples/starters/nodejs) | Express, Fastify, Hono, NestJS |
+| [starters/python](./examples/starters/python) | Flask, FastAPI, Django |
+| [starters/go](./examples/starters/go) | net/http, Gin |
+| [starters/ruby](./examples/starters/ruby) | Rails 8 |
+| [starters/php](./examples/starters/php) | Plain PHP, Laravel 12 |
+| [starters/java](./examples/starters/java), [dotnet](./examples/starters/dotnet), [elixir](./examples/starters/elixir), [swift](./examples/starters/swift), [rust](./examples/starters/rust), [deno](./examples/starters/deno), [bun](./examples/starters/bun) | one minimal app each |
+
## Getting Started
Each example lives in its own directory under `examples/` with its own README and setup instructions.
diff --git a/examples/starters/README.md b/examples/starters/README.md
new file mode 100644
index 0000000..79f9bb6
--- /dev/null
+++ b/examples/starters/README.md
@@ -0,0 +1,67 @@
+# Language starters
+
+Minimal, single-purpose applications — one per language or runtime. Each is the
+smallest thing that builds and answers an HTTP request, so it is a starting
+point rather than a showcase. For richer, production-shaped examples see the
+top level of [`examples/`](../).
+
+Every starter listens on `$PORT` and binds `0.0.0.0`, which is what makes it
+reachable once deployed.
+
+| Starter | Stack |
+|---|---|
+| [nodejs/express](./nodejs/express) | Express (npm) |
+| [nodejs/fastify](./nodejs/fastify) | Fastify (pnpm) |
+| [nodejs/hono](./nodejs/hono) | Hono (Bun) |
+| [nodejs/nestjs](./nodejs/nestjs) | NestJS |
+| [nextjs/app-router](./nextjs/app-router) | Next.js App Router |
+| [bun/bun-server](./bun/bun-server) | Bun native HTTP server |
+| [bun/elysia](./bun/elysia) | Elysia |
+| [deno](./deno) | Deno |
+| [vite/react](./vite/react) | Vite + React (static) |
+| [astro](./astro) | Astro (node adapter) |
+| [nuxt](./nuxt) | Nuxt 3 |
+| [sveltekit](./sveltekit) | SvelteKit (node adapter) |
+| [python/flask](./python/flask) | Flask + Gunicorn |
+| [python/fastapi](./python/fastapi) | FastAPI |
+| [python/django](./python/django) | Django + WhiteNoise |
+| [go/net-http](./go/net-http) | Go `net/http` |
+| [go/gin](./go/gin) | Gin |
+| [rust/actix](./rust/actix) | Actix Web |
+| [ruby/rails](./ruby/rails) | Rails 8 (API-only) |
+| [php/vanilla](./php/vanilla) | Plain PHP |
+| [php/laravel](./php/laravel) | Laravel 12 |
+| [java/spring-boot](./java/spring-boot) | Spring Boot 3 |
+| [dotnet/web](./dotnet/web) | ASP.NET Core minimal API |
+| [elixir/phoenix](./elixir/phoenix) | Phoenix 1.7 (API-only) |
+| [swift/vapor](./swift/vapor) | Vapor 4 |
+| [dockerfile](./dockerfile) | Custom multi-stage Dockerfile |
+
+## Verification
+
+`.github/workflows/starters.yml` builds every starter with nixpacks and waits
+for a real HTTP response — on push, on pull requests, and weekly, because base
+images and package registries move even when this directory does not.
+
+The workflow has two jobs. `supported` is blocking. `nixpacks-gaps` is a
+report: it probes the starters below and always exits green, writing the
+outcome to the run summary — including calling out any that have *started*
+building, which is the signal worth acting on.
+
+Those starters are correct and build fine with a current toolchain; nixpacks
+resolves runtimes from the Nix package set, which lags upstream. As of
+2026-08-01:
+
+| Starter | Why nixpacks cannot build it |
+|---|---|
+| `go/net-http`, `go/gin` | `go.mod` declares `go 1.24`; Nix ships an older Go and the toolchain auto-download fails |
+| `dotnet/web` | Nix provides dotnet-sdk 6.0.413 against a `net9.0` project |
+| `astro`, `nuxt` | Nix npm is older than the `npm >=9.6.5` these require |
+| `swift/vapor` | `swift: command not found` |
+| `deno` | no build plan for a bare `main.ts` |
+| `java/spring-boot` | no build plan for `build.gradle` without a wrapper |
+| `bun/*`, `nodejs/hono` | `bun: not found` |
+| `python/django` | container exits at startup under the generated start command |
+
+They are kept visible rather than deleted, so the day nixpacks catches up the
+CI tells us.
diff --git a/examples/starters/astro/astro.config.mjs b/examples/starters/astro/astro.config.mjs
new file mode 100644
index 0000000..750dba5
--- /dev/null
+++ b/examples/starters/astro/astro.config.mjs
@@ -0,0 +1,13 @@
+import node from '@astrojs/node';
+import { defineConfig } from 'astro/config';
+
+// `output: 'server'` needs an adapter; without one `astro build` fails.
+// The node adapter emits dist/server/entry.mjs, which is what `start` runs.
+export default defineConfig({
+ output: 'server',
+ adapter: node({ mode: 'standalone' }),
+ server: {
+ host: '0.0.0.0',
+ port: Number(process.env.PORT ?? 4321),
+ },
+});
diff --git a/examples/starters/astro/package.json b/examples/starters/astro/package.json
new file mode 100644
index 0000000..652bc47
--- /dev/null
+++ b/examples/starters/astro/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "temps-astro-example",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "start": "node ./dist/server/entry.mjs"
+ },
+ "dependencies": {
+ "@astrojs/node": "^9.0.0",
+ "astro": "^5.1.0"
+ }
+}
diff --git a/examples/starters/astro/src/pages/index.astro b/examples/starters/astro/src/pages/index.astro
new file mode 100644
index 0000000..1da3343
--- /dev/null
+++ b/examples/starters/astro/src/pages/index.astro
@@ -0,0 +1,14 @@
+---
+const message = "Hello from Astro on Temps!";
+---
+
+
+
+
+
+ Astro on Temps
+
+
+ {message}
+
+
diff --git a/examples/starters/bun/bun-server/index.ts b/examples/starters/bun/bun-server/index.ts
new file mode 100644
index 0000000..d20dd7a
--- /dev/null
+++ b/examples/starters/bun/bun-server/index.ts
@@ -0,0 +1,20 @@
+const port = parseInt(process.env.PORT || '3000');
+
+const server = Bun.serve({
+ port,
+ fetch(req) {
+ const url = new URL(req.url);
+
+ if (url.pathname === '/') {
+ return Response.json({ message: 'Hello from Bun on Temps!' });
+ }
+
+ if (url.pathname === '/health') {
+ return Response.json({ status: 'ok' });
+ }
+
+ return new Response('Not Found', { status: 404 });
+ },
+});
+
+console.log(`Listening on port ${server.port}`);
diff --git a/examples/starters/bun/bun-server/package.json b/examples/starters/bun/bun-server/package.json
new file mode 100644
index 0000000..1ef94a9
--- /dev/null
+++ b/examples/starters/bun/bun-server/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "temps-bun-server-example",
+ "version": "1.0.0",
+ "scripts": {
+ "start": "bun run index.ts"
+ }
+}
diff --git a/examples/starters/bun/elysia/package.json b/examples/starters/bun/elysia/package.json
new file mode 100644
index 0000000..1d9f7b9
--- /dev/null
+++ b/examples/starters/bun/elysia/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "temps-elysia-example",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "bun run src/index.ts"
+ },
+ "dependencies": {
+ "elysia": "^1.3.2"
+ }
+}
diff --git a/examples/starters/bun/elysia/src/index.ts b/examples/starters/bun/elysia/src/index.ts
new file mode 100644
index 0000000..51b8aac
--- /dev/null
+++ b/examples/starters/bun/elysia/src/index.ts
@@ -0,0 +1,10 @@
+import { Elysia, t } from 'elysia';
+
+const port = parseInt(process.env.PORT || '3000');
+
+const app = new Elysia()
+ .get('/', () => ({ message: 'Hello from Elysia on Temps!' }))
+ .get('/health', () => ({ status: 'ok' }))
+ .listen(port);
+
+console.log(`Listening on port ${app.server?.port}`);
diff --git a/examples/starters/deno/main.ts b/examples/starters/deno/main.ts
new file mode 100644
index 0000000..adcce4e
--- /dev/null
+++ b/examples/starters/deno/main.ts
@@ -0,0 +1,24 @@
+const port = parseInt(Deno.env.get("PORT") || "3000");
+
+const server = Deno.serve({
+ port,
+ handler: (req) => {
+ const url = new URL(req.url);
+
+ if (url.pathname === "/") {
+ return new Response(JSON.stringify({ message: "Hello from Deno on Temps!" }), {
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ if (url.pathname === "/health") {
+ return new Response(JSON.stringify({ status: "ok" }), {
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ return new Response("Not Found", { status: 404 });
+ },
+});
+
+console.log(`Listening on port ${server.port}`);
diff --git a/examples/starters/dockerfile/Dockerfile b/examples/starters/dockerfile/Dockerfile
new file mode 100644
index 0000000..3d6cfc1
--- /dev/null
+++ b/examples/starters/dockerfile/Dockerfile
@@ -0,0 +1,18 @@
+# Multi-stage build example
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+
+FROM node:22-alpine
+RUN addgroup -g 1001 appgroup && adduser -D -u 1001 -G appgroup appuser
+WORKDIR /app
+COPY --from=builder /app/dist ./dist
+COPY --from=builder /app/node_modules ./node_modules
+COPY --from=builder /app/package.json ./
+USER appuser
+EXPOSE 3000
+ENV PORT=3000
+CMD ["node", "dist/index.js"]
diff --git a/examples/starters/dockerfile/package.json b/examples/starters/dockerfile/package.json
new file mode 100644
index 0000000..2c85ca4
--- /dev/null
+++ b/examples/starters/dockerfile/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "temps-dockerfile-example",
+ "version": "1.0.0",
+ "scripts": {
+ "build": "echo 'Build complete' && mkdir -p dist && echo '{\"message\":\"Hello from custom Dockerfile!\"}' > dist/index.html",
+ "start": "npx serve dist -l 3000"
+ },
+ "dependencies": {
+ "serve": "^14.2.0"
+ }
+}
diff --git a/examples/starters/dotnet/web/Program.cs b/examples/starters/dotnet/web/Program.cs
new file mode 100644
index 0000000..59226d5
--- /dev/null
+++ b/examples/starters/dotnet/web/Program.cs
@@ -0,0 +1,10 @@
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+app.MapGet("/", () => new { message = "Hello from .NET on Temps!" });
+app.MapGet("/health", () => new { status = "ok" });
+
+var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
+app.Urls.Add($"http://0.0.0.0:{port}");
+
+app.Run();
diff --git a/examples/starters/dotnet/web/web.csproj b/examples/starters/dotnet/web/web.csproj
new file mode 100644
index 0000000..f48b2ae
--- /dev/null
+++ b/examples/starters/dotnet/web/web.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net9.0
+ enable
+ enable
+ TempsDotnetExample
+
+
+
diff --git a/examples/starters/elixir/phoenix/config/config.exs b/examples/starters/elixir/phoenix/config/config.exs
new file mode 100644
index 0000000..02858b1
--- /dev/null
+++ b/examples/starters/elixir/phoenix/config/config.exs
@@ -0,0 +1,9 @@
+import Config
+
+config :hello, HelloWeb.Endpoint,
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [formats: [json: HelloWeb.ErrorJSON], layout: false]
+
+config :phoenix, :json_library, Jason
+
+config :logger, level: :info
diff --git a/examples/starters/elixir/phoenix/config/runtime.exs b/examples/starters/elixir/phoenix/config/runtime.exs
new file mode 100644
index 0000000..1f29523
--- /dev/null
+++ b/examples/starters/elixir/phoenix/config/runtime.exs
@@ -0,0 +1,9 @@
+import Config
+
+# Temps injects PORT. Binding 0.0.0.0 is what makes the container reachable.
+port = String.to_integer(System.get_env("PORT") || "4000")
+
+config :hello, HelloWeb.Endpoint,
+ http: [ip: {0, 0, 0, 0}, port: port],
+ secret_key_base: System.get_env("SECRET_KEY_BASE") || String.duplicate("a", 64),
+ server: true
diff --git a/examples/starters/elixir/phoenix/lib/hello/application.ex b/examples/starters/elixir/phoenix/lib/hello/application.ex
new file mode 100644
index 0000000..d21065b
--- /dev/null
+++ b/examples/starters/elixir/phoenix/lib/hello/application.ex
@@ -0,0 +1,11 @@
+defmodule Hello.Application do
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [HelloWeb.Endpoint]
+ Supervisor.start_link(children, strategy: :one_for_one, name: Hello.Supervisor)
+ end
+end
diff --git a/examples/starters/elixir/phoenix/lib/hello_web.ex b/examples/starters/elixir/phoenix/lib/hello_web.ex
new file mode 100644
index 0000000..5fa8af8
--- /dev/null
+++ b/examples/starters/elixir/phoenix/lib/hello_web.ex
@@ -0,0 +1,27 @@
+defmodule HelloWeb do
+ @moduledoc """
+ Entrypoint for the web layer. `use HelloWeb, :controller` and
+ `use HelloWeb, :router` expand to the boilerplate below.
+ """
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ import Plug.Conn
+ import Phoenix.Controller
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller, formats: [:json]
+
+ import Plug.Conn
+ end
+ end
+
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/examples/starters/elixir/phoenix/lib/hello_web/controllers/error_json.ex b/examples/starters/elixir/phoenix/lib/hello_web/controllers/error_json.ex
new file mode 100644
index 0000000..d74251c
--- /dev/null
+++ b/examples/starters/elixir/phoenix/lib/hello_web/controllers/error_json.ex
@@ -0,0 +1,10 @@
+defmodule HelloWeb.ErrorJSON do
+ @moduledoc """
+ Renders errors as JSON. Referenced by `render_errors` in config.exs, so it
+ must exist even when nothing ever fails.
+ """
+
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/examples/starters/elixir/phoenix/lib/hello_web/controllers/page_controller.ex b/examples/starters/elixir/phoenix/lib/hello_web/controllers/page_controller.ex
new file mode 100644
index 0000000..8427f76
--- /dev/null
+++ b/examples/starters/elixir/phoenix/lib/hello_web/controllers/page_controller.ex
@@ -0,0 +1,11 @@
+defmodule HelloWeb.PageController do
+ use HelloWeb, :controller
+
+ def home(conn, _params) do
+ json(conn, %{message: "Hello from Phoenix on Temps!"})
+ end
+
+ def health(conn, _params) do
+ json(conn, %{status: "ok"})
+ end
+end
diff --git a/examples/starters/elixir/phoenix/lib/hello_web/endpoint.ex b/examples/starters/elixir/phoenix/lib/hello_web/endpoint.ex
new file mode 100644
index 0000000..ce8996f
--- /dev/null
+++ b/examples/starters/elixir/phoenix/lib/hello_web/endpoint.ex
@@ -0,0 +1,10 @@
+defmodule HelloWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :hello
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug HelloWeb.Router
+end
diff --git a/examples/starters/elixir/phoenix/lib/hello_web/router.ex b/examples/starters/elixir/phoenix/lib/hello_web/router.ex
new file mode 100644
index 0000000..0568830
--- /dev/null
+++ b/examples/starters/elixir/phoenix/lib/hello_web/router.ex
@@ -0,0 +1,14 @@
+defmodule HelloWeb.Router do
+ use HelloWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", HelloWeb do
+ pipe_through :api
+
+ get "/", PageController, :home
+ get "/health", PageController, :health
+ end
+end
diff --git a/examples/starters/elixir/phoenix/mix.exs b/examples/starters/elixir/phoenix/mix.exs
new file mode 100644
index 0000000..7091f43
--- /dev/null
+++ b/examples/starters/elixir/phoenix/mix.exs
@@ -0,0 +1,28 @@
+defmodule Hello.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :hello,
+ version: "1.0.0",
+ elixir: "~> 1.17",
+ start_permanent: Mix.env() == :prod,
+ deps: deps()
+ ]
+ end
+
+ def application do
+ [
+ mod: {Hello.Application, []},
+ extra_applications: [:logger]
+ ]
+ end
+
+ defp deps do
+ [
+ {:phoenix, "~> 1.7.14"},
+ {:bandit, "~> 1.5"},
+ {:jason, "~> 1.4"}
+ ]
+ end
+end
diff --git a/examples/starters/go/gin/go.mod b/examples/starters/go/gin/go.mod
new file mode 100644
index 0000000..59f29da
--- /dev/null
+++ b/examples/starters/go/gin/go.mod
@@ -0,0 +1,5 @@
+module temps-gin-example
+
+go 1.24
+
+require github.com/gin-gonic/gin v1.10.0
diff --git a/examples/starters/go/gin/main.go b/examples/starters/go/gin/main.go
new file mode 100644
index 0000000..145204e
--- /dev/null
+++ b/examples/starters/go/gin/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "net/http"
+ "os"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8080"
+ }
+
+ r := gin.Default()
+
+ r.GET("/", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"message": "Hello from Gin on Temps!"})
+ })
+
+ r.GET("/health", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"status": "ok"})
+ })
+
+ r.Run(":" + port)
+}
diff --git a/examples/starters/go/net-http/go.mod b/examples/starters/go/net-http/go.mod
new file mode 100644
index 0000000..45dfdf9
--- /dev/null
+++ b/examples/starters/go/net-http/go.mod
@@ -0,0 +1,3 @@
+module temps-go-example
+
+go 1.24
diff --git a/examples/starters/go/net-http/main.go b/examples/starters/go/net-http/main.go
new file mode 100644
index 0000000..5d0c7fd
--- /dev/null
+++ b/examples/starters/go/net-http/main.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+)
+
+func main() {
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8080"
+ }
+
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{
+ "message": "Hello from Go on Temps!",
+ })
+ })
+
+ http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
+ })
+
+ fmt.Printf("Listening on port %s\n", port)
+ http.ListenAndServe(":"+port, nil)
+}
diff --git a/examples/starters/java/spring-boot/build.gradle b/examples/starters/java/spring-boot/build.gradle
new file mode 100644
index 0000000..05a1b54
--- /dev/null
+++ b/examples/starters/java/spring-boot/build.gradle
@@ -0,0 +1,24 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.4.1'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'com.example'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ sourceCompatibility = '21'
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+}
+
+springBoot {
+ mainClass = 'com.example.demo.DemoApplication'
+}
diff --git a/examples/starters/java/spring-boot/src/main/java/com/example/demo/DemoApplication.java b/examples/starters/java/spring-boot/src/main/java/com/example/demo/DemoApplication.java
new file mode 100644
index 0000000..07a1586
--- /dev/null
+++ b/examples/starters/java/spring-boot/src/main/java/com/example/demo/DemoApplication.java
@@ -0,0 +1,25 @@
+package com.example.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import java.util.Map;
+
+@SpringBootApplication
+@RestController
+public class DemoApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+ @GetMapping("/")
+ public Map home() {
+ return Map.of("message", "Hello from Spring Boot on Temps!");
+ }
+
+ @GetMapping("/health")
+ public Map health() {
+ return Map.of("status", "ok");
+ }
+}
diff --git a/examples/starters/java/spring-boot/src/main/resources/application.yaml b/examples/starters/java/spring-boot/src/main/resources/application.yaml
new file mode 100644
index 0000000..4a01fbf
--- /dev/null
+++ b/examples/starters/java/spring-boot/src/main/resources/application.yaml
@@ -0,0 +1,6 @@
+server:
+ port: ${PORT:8080}
+
+spring:
+ main:
+ web-application-type: reactive
diff --git a/examples/starters/nextjs/app-router/app/api/health/route.ts b/examples/starters/nextjs/app-router/app/api/health/route.ts
new file mode 100644
index 0000000..78b675f
--- /dev/null
+++ b/examples/starters/nextjs/app-router/app/api/health/route.ts
@@ -0,0 +1,5 @@
+import { NextResponse } from 'next/server';
+
+export function GET() {
+ return NextResponse.json({ status: 'ok' });
+}
diff --git a/examples/starters/nextjs/app-router/app/layout.tsx b/examples/starters/nextjs/app-router/app/layout.tsx
new file mode 100644
index 0000000..890681f
--- /dev/null
+++ b/examples/starters/nextjs/app-router/app/layout.tsx
@@ -0,0 +1,18 @@
+// Next.js App Router requires a root layout; without it `next build` fails
+// with "page.tsx doesn't have a root layout".
+export const metadata = {
+ title: "Temps Next.js Example",
+ description: "Next.js App Router running on Temps",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/starters/nextjs/app-router/app/page.tsx b/examples/starters/nextjs/app-router/app/page.tsx
new file mode 100644
index 0000000..03c7a0f
--- /dev/null
+++ b/examples/starters/nextjs/app-router/app/page.tsx
@@ -0,0 +1,8 @@
+export default function Home() {
+ return (
+
+ Hello from Next.js on Temps!
+ This is a Next.js App Router example.
+
+ );
+}
diff --git a/examples/starters/nextjs/app-router/next.config.js b/examples/starters/nextjs/app-router/next.config.js
new file mode 100644
index 0000000..b408897
--- /dev/null
+++ b/examples/starters/nextjs/app-router/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: 'standalone',
+};
+
+module.exports = nextConfig;
diff --git a/examples/starters/nextjs/app-router/package.json b/examples/starters/nextjs/app-router/package.json
new file mode 100644
index 0000000..2356edb
--- /dev/null
+++ b/examples/starters/nextjs/app-router/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "temps-nextjs-example",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "^15.3.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/examples/starters/nextjs/app-router/tsconfig.json b/examples/starters/nextjs/app-router/tsconfig.json
new file mode 100644
index 0000000..cae02ec
--- /dev/null
+++ b/examples/starters/nextjs/app-router/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [{ "name": "next" }]
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/examples/starters/nodejs/express/package.json b/examples/starters/nodejs/express/package.json
new file mode 100644
index 0000000..aabc448
--- /dev/null
+++ b/examples/starters/nodejs/express/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "temps-express-example",
+ "version": "1.0.0",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^5.1.0"
+ }
+}
diff --git a/examples/starters/nodejs/express/server.js b/examples/starters/nodejs/express/server.js
new file mode 100644
index 0000000..4009e87
--- /dev/null
+++ b/examples/starters/nodejs/express/server.js
@@ -0,0 +1,15 @@
+const express = require('express');
+const app = express();
+const port = process.env.PORT || 3000;
+
+app.get('/', (req, res) => {
+ res.json({ message: 'Hello from Express on Temps!' });
+});
+
+app.get('/health', (req, res) => {
+ res.json({ status: 'ok' });
+});
+
+app.listen(port, () => {
+ console.log(`Listening on port ${port}`);
+});
diff --git a/examples/starters/nodejs/fastify/package.json b/examples/starters/nodejs/fastify/package.json
new file mode 100644
index 0000000..68081f4
--- /dev/null
+++ b/examples/starters/nodejs/fastify/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "temps-fastify-example",
+ "version": "1.0.0",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "fastify": "^5.3.3"
+ }
+}
diff --git a/examples/starters/nodejs/fastify/server.js b/examples/starters/nodejs/fastify/server.js
new file mode 100644
index 0000000..c5dec88
--- /dev/null
+++ b/examples/starters/nodejs/fastify/server.js
@@ -0,0 +1,17 @@
+const fastify = require('fastify')({ logger: true });
+const port = process.env.PORT || 3000;
+
+fastify.get('/', async () => {
+ return { message: 'Hello from Fastify on Temps!' };
+});
+
+fastify.get('/health', async () => {
+ return { status: 'ok' };
+});
+
+fastify.listen({ port, host: '0.0.0.0' }, (err) => {
+ if (err) {
+ fastify.log.error(err);
+ process.exit(1);
+ }
+});
diff --git a/examples/starters/nodejs/hono/package.json b/examples/starters/nodejs/hono/package.json
new file mode 100644
index 0000000..f461408
--- /dev/null
+++ b/examples/starters/nodejs/hono/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "temps-hono-example",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "bun src/index.ts"
+ },
+ "dependencies": {
+ "hono": "^4.7.0"
+ }
+}
diff --git a/examples/starters/nodejs/hono/src/index.ts b/examples/starters/nodejs/hono/src/index.ts
new file mode 100644
index 0000000..6c0a7f5
--- /dev/null
+++ b/examples/starters/nodejs/hono/src/index.ts
@@ -0,0 +1,8 @@
+import { Hono } from 'hono';
+
+const app = new Hono();
+
+app.get('/', (c) => c.json({ message: 'Hello from Hono on Temps!' }));
+app.get('/health', (c) => c.json({ status: 'ok' }));
+
+export default app;
diff --git a/examples/starters/nodejs/nestjs/package.json b/examples/starters/nodejs/nestjs/package.json
new file mode 100644
index 0000000..eeeb1ac
--- /dev/null
+++ b/examples/starters/nodejs/nestjs/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "temps-nestjs-example",
+ "version": "1.0.0",
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/main"
+ },
+ "dependencies": {
+ "@nestjs/common": "^11.0.0",
+ "@nestjs/core": "^11.0.0",
+ "@nestjs/platform-express": "^11.0.0",
+ "reflect-metadata": "^0.2.0",
+ "rxjs": "^7.8.0"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "typescript": "^5.0.0",
+ "@types/express": "^5.0.0"
+ }
+}
diff --git a/examples/starters/nodejs/nestjs/src/app.controller.ts b/examples/starters/nodejs/nestjs/src/app.controller.ts
new file mode 100644
index 0000000..c8f6874
--- /dev/null
+++ b/examples/starters/nodejs/nestjs/src/app.controller.ts
@@ -0,0 +1,14 @@
+import { Controller, Get } from '@nestjs/common';
+
+@Controller()
+export class AppController {
+ @Get()
+ home() {
+ return { message: 'Hello from NestJS on Temps!' };
+ }
+
+ @Get('health')
+ health() {
+ return { status: 'ok' };
+ }
+}
diff --git a/examples/starters/nodejs/nestjs/src/app.module.ts b/examples/starters/nodejs/nestjs/src/app.module.ts
new file mode 100644
index 0000000..848d4aa
--- /dev/null
+++ b/examples/starters/nodejs/nestjs/src/app.module.ts
@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+import { AppController } from './app.controller';
+
+@Module({
+ controllers: [AppController],
+})
+export class AppModule {}
diff --git a/examples/starters/nodejs/nestjs/src/main.ts b/examples/starters/nodejs/nestjs/src/main.ts
new file mode 100644
index 0000000..41a926f
--- /dev/null
+++ b/examples/starters/nodejs/nestjs/src/main.ts
@@ -0,0 +1,10 @@
+import { NestFactory } from '@nestjs/core';
+import { AppModule } from './app.module';
+
+async function bootstrap() {
+ const app = await NestFactory.create(AppModule);
+ const port = process.env.PORT || 3000;
+ await app.listen(port);
+ console.log(`Listening on port ${port}`);
+}
+bootstrap();
diff --git a/examples/starters/nodejs/nestjs/tsconfig.json b/examples/starters/nodejs/nestjs/tsconfig.json
new file mode 100644
index 0000000..1d73ce1
--- /dev/null
+++ b/examples/starters/nodejs/nestjs/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "ES2022",
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "strictNullChecks": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/examples/starters/nuxt/app.vue b/examples/starters/nuxt/app.vue
new file mode 100644
index 0000000..599282f
--- /dev/null
+++ b/examples/starters/nuxt/app.vue
@@ -0,0 +1,5 @@
+
+
+
Hello from Nuxt on Temps!
+
+
diff --git a/examples/starters/nuxt/nuxt.config.ts b/examples/starters/nuxt/nuxt.config.ts
new file mode 100644
index 0000000..5f89968
--- /dev/null
+++ b/examples/starters/nuxt/nuxt.config.ts
@@ -0,0 +1,8 @@
+export default defineNuxtConfig({
+ devtools: { enabled: false },
+ server: {
+ host: '0.0.0.0',
+ port: 3000,
+ },
+ compatibilityDate: '2024-11-01',
+});
diff --git a/examples/starters/nuxt/package.json b/examples/starters/nuxt/package.json
new file mode 100644
index 0000000..1cad28c
--- /dev/null
+++ b/examples/starters/nuxt/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "temps-nuxt-example",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "nuxt dev",
+ "build": "nuxt build",
+ "start": "node .output/server/index.mjs"
+ },
+ "dependencies": {
+ "nuxt": "^3.15.0",
+ "vue": "^3.5.0"
+ }
+}
diff --git a/examples/starters/php/laravel/app/.gitkeep b/examples/starters/php/laravel/app/.gitkeep
new file mode 100644
index 0000000..e634026
--- /dev/null
+++ b/examples/starters/php/laravel/app/.gitkeep
@@ -0,0 +1,3 @@
+handleCommand(new ArgvInput);
+
+exit($status);
diff --git a/examples/starters/php/laravel/bootstrap/app.php b/examples/starters/php/laravel/bootstrap/app.php
new file mode 100644
index 0000000..75450b1
--- /dev/null
+++ b/examples/starters/php/laravel/bootstrap/app.php
@@ -0,0 +1,20 @@
+withRouting(
+ // Laravel 11+ prefixes API routes with /api by default. This example
+ // serves its routes at the root, like every other example here, so the
+ // prefix is cleared explicitly.
+ api: __DIR__.'/../routes/api.php',
+ apiPrefix: '',
+ )
+ ->withMiddleware(function (Middleware $middleware) {
+ //
+ })
+ ->withExceptions(function (Exceptions $exceptions) {
+ //
+ })->create();
diff --git a/examples/starters/php/laravel/bootstrap/cache/.gitignore b/examples/starters/php/laravel/bootstrap/cache/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/examples/starters/php/laravel/bootstrap/cache/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/examples/starters/php/laravel/bootstrap/providers.php b/examples/starters/php/laravel/bootstrap/providers.php
new file mode 100644
index 0000000..3ac44ad
--- /dev/null
+++ b/examples/starters/php/laravel/bootstrap/providers.php
@@ -0,0 +1,5 @@
+handleRequest(Request::capture());
diff --git a/examples/starters/php/laravel/routes/api.php b/examples/starters/php/laravel/routes/api.php
new file mode 100644
index 0000000..91d1a58
--- /dev/null
+++ b/examples/starters/php/laravel/routes/api.php
@@ -0,0 +1,11 @@
+json(['message' => 'Hello from Laravel on Temps!']);
+});
+
+Route::get('/health', function () {
+ return response()->json(['status' => 'ok']);
+});
diff --git a/examples/starters/php/laravel/storage/framework/cache/data/.gitignore b/examples/starters/php/laravel/storage/framework/cache/data/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/examples/starters/php/laravel/storage/framework/cache/data/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/examples/starters/php/laravel/storage/framework/sessions/.gitignore b/examples/starters/php/laravel/storage/framework/sessions/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/examples/starters/php/laravel/storage/framework/sessions/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/examples/starters/php/laravel/storage/framework/views/.gitignore b/examples/starters/php/laravel/storage/framework/views/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/examples/starters/php/laravel/storage/framework/views/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/examples/starters/php/laravel/storage/logs/.gitignore b/examples/starters/php/laravel/storage/logs/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/examples/starters/php/laravel/storage/logs/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/examples/starters/php/vanilla/index.php b/examples/starters/php/vanilla/index.php
new file mode 100644
index 0000000..36a9c52
--- /dev/null
+++ b/examples/starters/php/vanilla/index.php
@@ -0,0 +1,17 @@
+ 'ok']);
+ exit;
+}
+
+if ($path === '/') {
+ echo json_encode(['message' => 'Hello from PHP on Temps!']);
+ exit;
+}
+
+http_response_code(404);
+echo json_encode(['error' => 'Not found']);
diff --git a/examples/starters/php/vanilla/nixpacks.toml b/examples/starters/php/vanilla/nixpacks.toml
new file mode 100644
index 0000000..fae45cd
--- /dev/null
+++ b/examples/starters/php/vanilla/nixpacks.toml
@@ -0,0 +1,2 @@
+[start]
+cmd = "php -S 0.0.0.0:${PORT:-8000} -t ."
diff --git a/examples/starters/python/django/manage.py b/examples/starters/python/django/manage.py
new file mode 100644
index 0000000..72af24a
--- /dev/null
+++ b/examples/starters/python/django/manage.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/examples/starters/python/django/mysite/asgi.py b/examples/starters/python/django/mysite/asgi.py
new file mode 100644
index 0000000..2bafcc2
--- /dev/null
+++ b/examples/starters/python/django/mysite/asgi.py
@@ -0,0 +1,5 @@
+import os
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
+application = get_asgi_application()
diff --git a/examples/starters/python/django/mysite/settings.py b/examples/starters/python/django/mysite/settings.py
new file mode 100644
index 0000000..ab75da9
--- /dev/null
+++ b/examples/starters/python/django/mysite/settings.py
@@ -0,0 +1,36 @@
+import os
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Read the key from the environment, with a development-only fallback. A
+# committed key is a committed key even when it says "insecure" — set
+# SECRET_KEY in Temps for anything you actually deploy.
+SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only-do-not-deploy")
+
+DEBUG = os.environ.get("DEBUG", "False") == "True"
+
+ALLOWED_HOSTS = ["*"]
+
+INSTALLED_APPS = [
+ "django.contrib.contenttypes",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "whitenoise.middleware.WhiteNoiseMiddleware",
+ "django.middleware.common.CommonMiddleware",
+]
+
+ROOT_URLCONF = "mysite.urls"
+
+STATIC_URL = "/static/"
+STATIC_ROOT = BASE_DIR / "staticfiles"
+STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+
+WSGI_APPLICATION = "mysite.wsgi.application"
+
+DATABASES = {}
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/examples/starters/python/django/mysite/urls.py b/examples/starters/python/django/mysite/urls.py
new file mode 100644
index 0000000..6e27645
--- /dev/null
+++ b/examples/starters/python/django/mysite/urls.py
@@ -0,0 +1,16 @@
+from django.urls import path
+from django.http import JsonResponse
+
+
+def home(request):
+ return JsonResponse({"message": "Hello from Django on Temps!"})
+
+
+def health(request):
+ return JsonResponse({"status": "ok"})
+
+
+urlpatterns = [
+ path("", home),
+ path("health", health),
+]
diff --git a/examples/starters/python/django/mysite/wsgi.py b/examples/starters/python/django/mysite/wsgi.py
new file mode 100644
index 0000000..f4b8d49
--- /dev/null
+++ b/examples/starters/python/django/mysite/wsgi.py
@@ -0,0 +1,5 @@
+import os
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
+application = get_wsgi_application()
diff --git a/examples/starters/python/django/requirements.txt b/examples/starters/python/django/requirements.txt
new file mode 100644
index 0000000..c4e3d2d
--- /dev/null
+++ b/examples/starters/python/django/requirements.txt
@@ -0,0 +1,4 @@
+django>=5.2
+gunicorn>=23.0.0
+whitenoise>=6.9.0
+psycopg2-binary>=2.9.0
diff --git a/examples/starters/python/fastapi/main.py b/examples/starters/python/fastapi/main.py
new file mode 100644
index 0000000..6f35305
--- /dev/null
+++ b/examples/starters/python/fastapi/main.py
@@ -0,0 +1,21 @@
+from fastapi import FastAPI
+import os
+
+app = FastAPI()
+
+
+@app.get("/")
+def read_root():
+ return {"message": "Hello from FastAPI on Temps!"}
+
+
+@app.get("/health")
+def health():
+ return {"status": "ok"}
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ port = int(os.environ.get("PORT", 8000))
+ uvicorn.run(app, host="0.0.0.0", port=port)
diff --git a/examples/starters/python/fastapi/nixpacks.toml b/examples/starters/python/fastapi/nixpacks.toml
new file mode 100644
index 0000000..06a1d81
--- /dev/null
+++ b/examples/starters/python/fastapi/nixpacks.toml
@@ -0,0 +1,2 @@
+[start]
+cmd = "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}"
diff --git a/examples/starters/python/fastapi/requirements.txt b/examples/starters/python/fastapi/requirements.txt
new file mode 100644
index 0000000..50b0ab1
--- /dev/null
+++ b/examples/starters/python/fastapi/requirements.txt
@@ -0,0 +1,2 @@
+fastapi>=0.115.0
+uvicorn[standard]>=0.34.0
diff --git a/examples/starters/python/flask/app.py b/examples/starters/python/flask/app.py
new file mode 100644
index 0000000..97ca735
--- /dev/null
+++ b/examples/starters/python/flask/app.py
@@ -0,0 +1,19 @@
+from flask import Flask, jsonify
+import os
+
+app = Flask(__name__)
+
+
+@app.route("/")
+def hello():
+ return jsonify(message="Hello from Flask on Temps!")
+
+
+@app.route("/health")
+def health():
+ return jsonify(status="ok")
+
+
+if __name__ == "__main__":
+ port = int(os.environ.get("PORT", 5000))
+ app.run(host="0.0.0.0", port=port)
diff --git a/examples/starters/python/flask/nixpacks.toml b/examples/starters/python/flask/nixpacks.toml
new file mode 100644
index 0000000..5698db7
--- /dev/null
+++ b/examples/starters/python/flask/nixpacks.toml
@@ -0,0 +1,2 @@
+[start]
+cmd = "gunicorn app:app --bind 0.0.0.0:${PORT:-5000}"
diff --git a/examples/starters/python/flask/requirements.txt b/examples/starters/python/flask/requirements.txt
new file mode 100644
index 0000000..1f42ac9
--- /dev/null
+++ b/examples/starters/python/flask/requirements.txt
@@ -0,0 +1,2 @@
+flask>=3.1.0
+gunicorn>=23.0.0
diff --git a/examples/starters/ruby/rails/Gemfile b/examples/starters/ruby/rails/Gemfile
new file mode 100644
index 0000000..d95f73f
--- /dev/null
+++ b/examples/starters/ruby/rails/Gemfile
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+ruby ">= 3.3.0"
+
+gem "rails", "~> 8.0.1"
+gem "puma", "~> 6.5"
diff --git a/examples/starters/ruby/rails/app/controllers/application_controller.rb b/examples/starters/ruby/rails/app/controllers/application_controller.rb
new file mode 100644
index 0000000..4ac8823
--- /dev/null
+++ b/examples/starters/ruby/rails/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::API
+end
diff --git a/examples/starters/ruby/rails/app/controllers/home_controller.rb b/examples/starters/ruby/rails/app/controllers/home_controller.rb
new file mode 100644
index 0000000..4f26aa4
--- /dev/null
+++ b/examples/starters/ruby/rails/app/controllers/home_controller.rb
@@ -0,0 +1,9 @@
+class HomeController < ApplicationController
+ def index
+ render json: { message: "Hello from Ruby on Rails on Temps!" }
+ end
+
+ def health
+ render json: { status: "ok" }
+ end
+end
diff --git a/examples/starters/ruby/rails/bin/rails b/examples/starters/ruby/rails/bin/rails
new file mode 100755
index 0000000..efc0377
--- /dev/null
+++ b/examples/starters/ruby/rails/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/examples/starters/ruby/rails/config.ru b/examples/starters/ruby/rails/config.ru
new file mode 100644
index 0000000..79630bc
--- /dev/null
+++ b/examples/starters/ruby/rails/config.ru
@@ -0,0 +1,3 @@
+require_relative "config/environment"
+
+run Rails.application
diff --git a/examples/starters/ruby/rails/config/application.rb b/examples/starters/ruby/rails/config/application.rb
new file mode 100644
index 0000000..1ccdba3
--- /dev/null
+++ b/examples/starters/ruby/rails/config/application.rb
@@ -0,0 +1,21 @@
+require_relative "boot"
+
+require "rails"
+require "action_controller/railtie"
+
+module TempsRailsExample
+ class Application < Rails::Application
+ config.load_defaults 8.0
+
+ # API-only keeps the example free of an asset pipeline.
+ config.api_only = true
+ config.eager_load = true
+
+ # Real apps read this from credentials; the fallback exists so
+ # `docker run` with no configuration still boots.
+ config.secret_key_base = ENV.fetch("SECRET_KEY_BASE", "temps-demo-secret-key-base")
+
+ config.hosts.clear
+ config.logger = Logger.new($stdout)
+ end
+end
diff --git a/examples/starters/ruby/rails/config/boot.rb b/examples/starters/ruby/rails/config/boot.rb
new file mode 100644
index 0000000..93f96d5
--- /dev/null
+++ b/examples/starters/ruby/rails/config/boot.rb
@@ -0,0 +1,3 @@
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+require "bundler/setup"
diff --git a/examples/starters/ruby/rails/config/environment.rb b/examples/starters/ruby/rails/config/environment.rb
new file mode 100644
index 0000000..73a3979
--- /dev/null
+++ b/examples/starters/ruby/rails/config/environment.rb
@@ -0,0 +1,3 @@
+require_relative "application"
+
+Rails.application.initialize!
diff --git a/examples/starters/ruby/rails/config/environments/development.rb b/examples/starters/ruby/rails/config/environments/development.rb
new file mode 100644
index 0000000..de11987
--- /dev/null
+++ b/examples/starters/ruby/rails/config/environments/development.rb
@@ -0,0 +1,5 @@
+Rails.application.configure do
+ config.enable_reloading = true
+ config.eager_load = false
+ config.consider_all_requests_local = true
+end
diff --git a/examples/starters/ruby/rails/config/environments/production.rb b/examples/starters/ruby/rails/config/environments/production.rb
new file mode 100644
index 0000000..1c89cf0
--- /dev/null
+++ b/examples/starters/ruby/rails/config/environments/production.rb
@@ -0,0 +1,6 @@
+Rails.application.configure do
+ config.enable_reloading = false
+ config.eager_load = true
+ config.consider_all_requests_local = false
+ config.log_level = :info
+end
diff --git a/examples/starters/ruby/rails/config/routes.rb b/examples/starters/ruby/rails/config/routes.rb
new file mode 100644
index 0000000..fc09e04
--- /dev/null
+++ b/examples/starters/ruby/rails/config/routes.rb
@@ -0,0 +1,4 @@
+Rails.application.routes.draw do
+ get "/" => "home#index"
+ get "/health" => "home#health"
+end
diff --git a/examples/starters/rust/actix/Cargo.toml b/examples/starters/rust/actix/Cargo.toml
new file mode 100644
index 0000000..9965ddd
--- /dev/null
+++ b/examples/starters/rust/actix/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "temps-actix-example"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+actix-web = "4"
+serde_json = "1"
diff --git a/examples/starters/rust/actix/src/main.rs b/examples/starters/rust/actix/src/main.rs
new file mode 100644
index 0000000..67349dc
--- /dev/null
+++ b/examples/starters/rust/actix/src/main.rs
@@ -0,0 +1,26 @@
+use actix_web::{web, App, HttpServer, HttpResponse};
+use serde_json::json;
+use std::env;
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ let port: u16 = env::var("PORT")
+ .unwrap_or_else(|_| "8080".to_string())
+ .parse()
+ .expect("PORT must be a number");
+
+ println!("Listening on port {}", port);
+
+ HttpServer::new(|| {
+ App::new()
+ .route("/", web::get().to(|| async {
+ HttpResponse::Ok().json(json!({"message": "Hello from Actix on Temps!"}))
+ }))
+ .route("/health", web::get().to(|| async {
+ HttpResponse::Ok().json(json!({"status": "ok"}))
+ }))
+ })
+ .bind(("0.0.0.0", port))?
+ .run()
+ .await
+}
diff --git a/examples/starters/sveltekit/package.json b/examples/starters/sveltekit/package.json
new file mode 100644
index 0000000..7fb099a
--- /dev/null
+++ b/examples/starters/sveltekit/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "temps-sveltekit-example",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "start": "node build/index.js"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-node": "^5.2.0",
+ "@sveltejs/kit": "^2.15.0",
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "svelte": "^5.16.0",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/examples/starters/sveltekit/src/app.html b/examples/starters/sveltekit/src/app.html
new file mode 100644
index 0000000..49d8e2c
--- /dev/null
+++ b/examples/starters/sveltekit/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Temps SvelteKit Example
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/examples/starters/sveltekit/src/routes/+page.server.ts b/examples/starters/sveltekit/src/routes/+page.server.ts
new file mode 100644
index 0000000..55d54a4
--- /dev/null
+++ b/examples/starters/sveltekit/src/routes/+page.server.ts
@@ -0,0 +1,8 @@
+export const prerender = false;
+export const ssr = true;
+
+const message = 'Hello from SvelteKit on Temps!';
+
+export function load() {
+ return { message };
+}
diff --git a/examples/starters/sveltekit/src/routes/+page.svelte b/examples/starters/sveltekit/src/routes/+page.svelte
new file mode 100644
index 0000000..0b1419c
--- /dev/null
+++ b/examples/starters/sveltekit/src/routes/+page.svelte
@@ -0,0 +1,5 @@
+
+
+{data.message}
diff --git a/examples/starters/sveltekit/svelte.config.js b/examples/starters/sveltekit/svelte.config.js
new file mode 100644
index 0000000..7799ddf
--- /dev/null
+++ b/examples/starters/sveltekit/svelte.config.js
@@ -0,0 +1,8 @@
+import adapter from '@sveltejs/adapter-node';
+
+// adapter-node emits build/index.js, which honours PORT and HOST at runtime.
+export default {
+ kit: {
+ adapter: adapter(),
+ },
+};
diff --git a/examples/starters/sveltekit/vite.config.js b/examples/starters/sveltekit/vite.config.js
new file mode 100644
index 0000000..2e920e4
--- /dev/null
+++ b/examples/starters/sveltekit/vite.config.js
@@ -0,0 +1,6 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [sveltekit()],
+});
diff --git a/examples/starters/swift/vapor/Package.swift b/examples/starters/swift/vapor/Package.swift
new file mode 100644
index 0000000..1ea5546
--- /dev/null
+++ b/examples/starters/swift/vapor/Package.swift
@@ -0,0 +1,21 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let package = Package(
+ name: "hello",
+ platforms: [
+ .macOS(.v13)
+ ],
+ dependencies: [
+ .package(url: "https://github.com/vapor/vapor.git", from: "4.101.0")
+ ],
+ targets: [
+ .executableTarget(
+ name: "App",
+ dependencies: [
+ .product(name: "Vapor", package: "vapor")
+ ],
+ path: "Sources/App"
+ )
+ ]
+)
diff --git a/examples/starters/swift/vapor/Sources/App/main.swift b/examples/starters/swift/vapor/Sources/App/main.swift
new file mode 100644
index 0000000..d62aa5a
--- /dev/null
+++ b/examples/starters/swift/vapor/Sources/App/main.swift
@@ -0,0 +1,20 @@
+import Vapor
+
+var env = try Environment.detect()
+let app = Application(env)
+defer { app.shutdown() }
+
+// Vapor defaults to 127.0.0.1, which is unreachable from outside a container.
+// Temps injects PORT, so bind to it on all interfaces.
+app.http.server.configuration.hostname = "0.0.0.0"
+app.http.server.configuration.port = Environment.get("PORT").flatMap(Int.init) ?? 8080
+
+app.get { _ in
+ ["message": "Hello from Vapor on Temps!"]
+}
+
+app.get("health") { _ in
+ ["status": "ok"]
+}
+
+try app.run()
diff --git a/examples/starters/vite/react/index.html b/examples/starters/vite/react/index.html
new file mode 100644
index 0000000..a9a1a3a
--- /dev/null
+++ b/examples/starters/vite/react/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite React on Temps
+
+
+
+
+
+
diff --git a/examples/starters/vite/react/package.json b/examples/starters/vite/react/package.json
new file mode 100644
index 0000000..585aac8
--- /dev/null
+++ b/examples/starters/vite/react/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "temps-vite-react-example",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^4.3.0",
+ "typescript": "^5.0.0",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/examples/starters/vite/react/src/App.tsx b/examples/starters/vite/react/src/App.tsx
new file mode 100644
index 0000000..22e0ba2
--- /dev/null
+++ b/examples/starters/vite/react/src/App.tsx
@@ -0,0 +1,10 @@
+function App() {
+ return (
+
+
Hello from Vite + React on Temps!
+
This is a Vite React example deployed as a static site.
+
+ );
+}
+
+export default App;
diff --git a/examples/starters/vite/react/src/main.tsx b/examples/starters/vite/react/src/main.tsx
new file mode 100644
index 0000000..3a8bd35
--- /dev/null
+++ b/examples/starters/vite/react/src/main.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/examples/starters/vite/react/tsconfig.json b/examples/starters/vite/react/tsconfig.json
new file mode 100644
index 0000000..3934b8f
--- /dev/null
+++ b/examples/starters/vite/react/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/examples/starters/vite/react/tsconfig.node.json b/examples/starters/vite/react/tsconfig.node.json
new file mode 100644
index 0000000..97ede7e
--- /dev/null
+++ b/examples/starters/vite/react/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/starters/vite/react/vite.config.ts b/examples/starters/vite/react/vite.config.ts
new file mode 100644
index 0000000..0466183
--- /dev/null
+++ b/examples/starters/vite/react/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+});