From 32ea5ff24b47f3acfee9ec0bb5878f301aaff91d Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 1 Aug 2026 21:44:08 +0200 Subject: [PATCH 1/3] feat: add minimal per-language starters under examples/starters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 26 single-purpose starters — one per language or runtime — as a counterpart to the existing production-shaped examples. Each is the smallest thing that builds and answers an HTTP request, listens on $PORT and binds 0.0.0.0. Nine of these did not build at all before this commit: astro, nuxt, sveltekit no package.json dotnet/web no .csproj elixir/phoenix no mix.exs, and an endpoint with duplicate at:/from: keys referencing a non-existent app nextjs/app-router no app/layout.tsx nodejs/nestjs `node dist/main` with no build script php/laravel no artisan, bootstrap/app.php or public/index.php ruby/rails config/application.rb contained JSON, not Ruby swift/vapor Package.swift written in YAML, bound to 127.0.0.1 Also fixed while verifying: Laravel served / as 404 (Laravel 11+ prefixes API routes with /api), and SvelteKit needed src/app.html for adapter-node. Django now reads SECRET_KEY from the environment rather than carrying a committed placeholder. Adds .github/workflows/starters.yml, which builds each starter with nixpacks and waits for a real HTTP response, on push, on PRs, and weekly. The matrix is split: 13 starters nixpacks can build are blocking, and 13 that it cannot are non-blocking with the reason documented — those are correct applications that fail because the Nix package set lags upstream (dotnet-sdk 6.0.413 against net9.0, npm older than astro requires, Go older than the `go 1.24` in go.mod, no Swift toolchain, no bun). Every starter was built and confirmed to return a response before being committed. --- .github/actions/build-and-serve/action.yml | 60 ++++++++++ .github/workflows/starters.yml | 104 ++++++++++++++++++ README.md | 15 +++ examples/starters/README.md | 63 +++++++++++ examples/starters/astro/astro.config.mjs | 13 +++ examples/starters/astro/package.json | 15 +++ examples/starters/astro/src/pages/index.astro | 14 +++ examples/starters/bun/bun-server/index.ts | 20 ++++ examples/starters/bun/bun-server/package.json | 7 ++ examples/starters/bun/elysia/package.json | 11 ++ examples/starters/bun/elysia/src/index.ts | 10 ++ examples/starters/deno/main.ts | 24 ++++ examples/starters/dockerfile/Dockerfile | 18 +++ examples/starters/dockerfile/package.json | 11 ++ examples/starters/dotnet/web/Program.cs | 10 ++ examples/starters/dotnet/web/nixpacks.toml | 2 + examples/starters/dotnet/web/web.csproj | 10 ++ .../starters/elixir/phoenix/config/config.exs | 9 ++ .../elixir/phoenix/config/runtime.exs | 9 ++ .../elixir/phoenix/lib/hello/application.ex | 11 ++ .../starters/elixir/phoenix/lib/hello_web.ex | 27 +++++ .../lib/hello_web/controllers/error_json.ex | 10 ++ .../hello_web/controllers/page_controller.ex | 11 ++ .../elixir/phoenix/lib/hello_web/endpoint.ex | 10 ++ .../elixir/phoenix/lib/hello_web/router.ex | 14 +++ examples/starters/elixir/phoenix/mix.exs | 28 +++++ examples/starters/go/gin/go.mod | 5 + examples/starters/go/gin/main.go | 27 +++++ examples/starters/go/net-http/go.mod | 3 + examples/starters/go/net-http/main.go | 30 +++++ .../starters/java/spring-boot/build.gradle | 24 ++++ .../com/example/demo/DemoApplication.java | 25 +++++ .../src/main/resources/application.yaml | 6 + .../nextjs/app-router/app/api/health/route.ts | 5 + .../starters/nextjs/app-router/app/layout.tsx | 18 +++ .../starters/nextjs/app-router/app/page.tsx | 8 ++ .../starters/nextjs/app-router/next.config.js | 6 + .../starters/nextjs/app-router/package.json | 20 ++++ .../starters/nextjs/app-router/tsconfig.json | 20 ++++ examples/starters/nodejs/express/package.json | 10 ++ examples/starters/nodejs/express/server.js | 15 +++ examples/starters/nodejs/fastify/package.json | 10 ++ examples/starters/nodejs/fastify/server.js | 17 +++ examples/starters/nodejs/hono/package.json | 11 ++ examples/starters/nodejs/hono/src/index.ts | 8 ++ examples/starters/nodejs/nestjs/package.json | 20 ++++ .../nodejs/nestjs/src/app.controller.ts | 14 +++ .../starters/nodejs/nestjs/src/app.module.ts | 7 ++ examples/starters/nodejs/nestjs/src/main.ts | 10 ++ examples/starters/nodejs/nestjs/tsconfig.json | 15 +++ examples/starters/nuxt/app.vue | 5 + examples/starters/nuxt/nuxt.config.ts | 8 ++ examples/starters/nuxt/package.json | 15 +++ examples/starters/php/laravel/app/.gitkeep | 3 + examples/starters/php/laravel/artisan | 16 +++ .../starters/php/laravel/bootstrap/app.php | 20 ++++ .../php/laravel/bootstrap/cache/.gitignore | 2 + .../php/laravel/bootstrap/providers.php | 5 + examples/starters/php/laravel/composer.json | 29 +++++ examples/starters/php/laravel/nixpacks.toml | 5 + .../starters/php/laravel/public/index.php | 13 +++ examples/starters/php/laravel/routes/api.php | 11 ++ .../storage/framework/cache/data/.gitignore | 2 + .../storage/framework/sessions/.gitignore | 2 + .../storage/framework/views/.gitignore | 2 + .../php/laravel/storage/logs/.gitignore | 2 + examples/starters/php/vanilla/index.php | 17 +++ examples/starters/php/vanilla/nixpacks.toml | 2 + examples/starters/python/django/manage.py | 9 ++ .../starters/python/django/mysite/asgi.py | 5 + .../starters/python/django/mysite/settings.py | 36 ++++++ .../starters/python/django/mysite/urls.py | 16 +++ .../starters/python/django/mysite/wsgi.py | 5 + examples/starters/python/django/nixpacks.toml | 5 + .../starters/python/django/requirements.txt | 4 + examples/starters/python/fastapi/main.py | 21 ++++ .../starters/python/fastapi/nixpacks.toml | 2 + .../starters/python/fastapi/requirements.txt | 2 + examples/starters/python/flask/app.py | 19 ++++ examples/starters/python/flask/nixpacks.toml | 2 + .../starters/python/flask/requirements.txt | 2 + examples/starters/ruby/rails/Gemfile | 6 + .../app/controllers/application_controller.rb | 2 + .../rails/app/controllers/home_controller.rb | 9 ++ examples/starters/ruby/rails/bin/rails | 4 + examples/starters/ruby/rails/config.ru | 3 + .../starters/ruby/rails/config/application.rb | 21 ++++ examples/starters/ruby/rails/config/boot.rb | 3 + .../starters/ruby/rails/config/environment.rb | 3 + .../rails/config/environments/development.rb | 5 + .../rails/config/environments/production.rb | 6 + examples/starters/ruby/rails/config/routes.rb | 4 + examples/starters/ruby/rails/nixpacks.toml | 5 + examples/starters/rust/actix/Cargo.toml | 8 ++ examples/starters/rust/actix/src/main.rs | 26 +++++ examples/starters/sveltekit/package.json | 18 +++ examples/starters/sveltekit/src/app.html | 12 ++ .../sveltekit/src/routes/+page.server.ts | 8 ++ .../sveltekit/src/routes/+page.svelte | 5 + examples/starters/sveltekit/svelte.config.js | 8 ++ examples/starters/sveltekit/vite.config.js | 6 + examples/starters/swift/vapor/Package.swift | 21 ++++ .../swift/vapor/Sources/App/main.swift | 20 ++++ examples/starters/vite/react/index.html | 12 ++ examples/starters/vite/react/package.json | 22 ++++ examples/starters/vite/react/src/App.tsx | 10 ++ examples/starters/vite/react/src/main.tsx | 9 ++ examples/starters/vite/react/tsconfig.json | 21 ++++ .../starters/vite/react/tsconfig.node.json | 11 ++ examples/starters/vite/react/vite.config.ts | 6 + 110 files changed, 1451 insertions(+) create mode 100644 .github/actions/build-and-serve/action.yml create mode 100644 .github/workflows/starters.yml create mode 100644 examples/starters/README.md create mode 100644 examples/starters/astro/astro.config.mjs create mode 100644 examples/starters/astro/package.json create mode 100644 examples/starters/astro/src/pages/index.astro create mode 100644 examples/starters/bun/bun-server/index.ts create mode 100644 examples/starters/bun/bun-server/package.json create mode 100644 examples/starters/bun/elysia/package.json create mode 100644 examples/starters/bun/elysia/src/index.ts create mode 100644 examples/starters/deno/main.ts create mode 100644 examples/starters/dockerfile/Dockerfile create mode 100644 examples/starters/dockerfile/package.json create mode 100644 examples/starters/dotnet/web/Program.cs create mode 100644 examples/starters/dotnet/web/nixpacks.toml create mode 100644 examples/starters/dotnet/web/web.csproj create mode 100644 examples/starters/elixir/phoenix/config/config.exs create mode 100644 examples/starters/elixir/phoenix/config/runtime.exs create mode 100644 examples/starters/elixir/phoenix/lib/hello/application.ex create mode 100644 examples/starters/elixir/phoenix/lib/hello_web.ex create mode 100644 examples/starters/elixir/phoenix/lib/hello_web/controllers/error_json.ex create mode 100644 examples/starters/elixir/phoenix/lib/hello_web/controllers/page_controller.ex create mode 100644 examples/starters/elixir/phoenix/lib/hello_web/endpoint.ex create mode 100644 examples/starters/elixir/phoenix/lib/hello_web/router.ex create mode 100644 examples/starters/elixir/phoenix/mix.exs create mode 100644 examples/starters/go/gin/go.mod create mode 100644 examples/starters/go/gin/main.go create mode 100644 examples/starters/go/net-http/go.mod create mode 100644 examples/starters/go/net-http/main.go create mode 100644 examples/starters/java/spring-boot/build.gradle create mode 100644 examples/starters/java/spring-boot/src/main/java/com/example/demo/DemoApplication.java create mode 100644 examples/starters/java/spring-boot/src/main/resources/application.yaml create mode 100644 examples/starters/nextjs/app-router/app/api/health/route.ts create mode 100644 examples/starters/nextjs/app-router/app/layout.tsx create mode 100644 examples/starters/nextjs/app-router/app/page.tsx create mode 100644 examples/starters/nextjs/app-router/next.config.js create mode 100644 examples/starters/nextjs/app-router/package.json create mode 100644 examples/starters/nextjs/app-router/tsconfig.json create mode 100644 examples/starters/nodejs/express/package.json create mode 100644 examples/starters/nodejs/express/server.js create mode 100644 examples/starters/nodejs/fastify/package.json create mode 100644 examples/starters/nodejs/fastify/server.js create mode 100644 examples/starters/nodejs/hono/package.json create mode 100644 examples/starters/nodejs/hono/src/index.ts create mode 100644 examples/starters/nodejs/nestjs/package.json create mode 100644 examples/starters/nodejs/nestjs/src/app.controller.ts create mode 100644 examples/starters/nodejs/nestjs/src/app.module.ts create mode 100644 examples/starters/nodejs/nestjs/src/main.ts create mode 100644 examples/starters/nodejs/nestjs/tsconfig.json create mode 100644 examples/starters/nuxt/app.vue create mode 100644 examples/starters/nuxt/nuxt.config.ts create mode 100644 examples/starters/nuxt/package.json create mode 100644 examples/starters/php/laravel/app/.gitkeep create mode 100755 examples/starters/php/laravel/artisan create mode 100644 examples/starters/php/laravel/bootstrap/app.php create mode 100644 examples/starters/php/laravel/bootstrap/cache/.gitignore create mode 100644 examples/starters/php/laravel/bootstrap/providers.php create mode 100644 examples/starters/php/laravel/composer.json create mode 100644 examples/starters/php/laravel/nixpacks.toml create mode 100644 examples/starters/php/laravel/public/index.php create mode 100644 examples/starters/php/laravel/routes/api.php create mode 100644 examples/starters/php/laravel/storage/framework/cache/data/.gitignore create mode 100644 examples/starters/php/laravel/storage/framework/sessions/.gitignore create mode 100644 examples/starters/php/laravel/storage/framework/views/.gitignore create mode 100644 examples/starters/php/laravel/storage/logs/.gitignore create mode 100644 examples/starters/php/vanilla/index.php create mode 100644 examples/starters/php/vanilla/nixpacks.toml create mode 100644 examples/starters/python/django/manage.py create mode 100644 examples/starters/python/django/mysite/asgi.py create mode 100644 examples/starters/python/django/mysite/settings.py create mode 100644 examples/starters/python/django/mysite/urls.py create mode 100644 examples/starters/python/django/mysite/wsgi.py create mode 100644 examples/starters/python/django/nixpacks.toml create mode 100644 examples/starters/python/django/requirements.txt create mode 100644 examples/starters/python/fastapi/main.py create mode 100644 examples/starters/python/fastapi/nixpacks.toml create mode 100644 examples/starters/python/fastapi/requirements.txt create mode 100644 examples/starters/python/flask/app.py create mode 100644 examples/starters/python/flask/nixpacks.toml create mode 100644 examples/starters/python/flask/requirements.txt create mode 100644 examples/starters/ruby/rails/Gemfile create mode 100644 examples/starters/ruby/rails/app/controllers/application_controller.rb create mode 100644 examples/starters/ruby/rails/app/controllers/home_controller.rb create mode 100755 examples/starters/ruby/rails/bin/rails create mode 100644 examples/starters/ruby/rails/config.ru create mode 100644 examples/starters/ruby/rails/config/application.rb create mode 100644 examples/starters/ruby/rails/config/boot.rb create mode 100644 examples/starters/ruby/rails/config/environment.rb create mode 100644 examples/starters/ruby/rails/config/environments/development.rb create mode 100644 examples/starters/ruby/rails/config/environments/production.rb create mode 100644 examples/starters/ruby/rails/config/routes.rb create mode 100644 examples/starters/ruby/rails/nixpacks.toml create mode 100644 examples/starters/rust/actix/Cargo.toml create mode 100644 examples/starters/rust/actix/src/main.rs create mode 100644 examples/starters/sveltekit/package.json create mode 100644 examples/starters/sveltekit/src/app.html create mode 100644 examples/starters/sveltekit/src/routes/+page.server.ts create mode 100644 examples/starters/sveltekit/src/routes/+page.svelte create mode 100644 examples/starters/sveltekit/svelte.config.js create mode 100644 examples/starters/sveltekit/vite.config.js create mode 100644 examples/starters/swift/vapor/Package.swift create mode 100644 examples/starters/swift/vapor/Sources/App/main.swift create mode 100644 examples/starters/vite/react/index.html create mode 100644 examples/starters/vite/react/package.json create mode 100644 examples/starters/vite/react/src/App.tsx create mode 100644 examples/starters/vite/react/src/main.tsx create mode 100644 examples/starters/vite/react/tsconfig.json create mode 100644 examples/starters/vite/react/tsconfig.node.json create mode 100644 examples/starters/vite/react/vite.config.ts 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..7795618 --- /dev/null +++ b/.github/workflows/starters.yml @@ -0,0 +1,104 @@ +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 }} + + # Examples that are correct but which nixpacks cannot currently build. + # + # Every one of these was confirmed to build and serve with a modern + # toolchain; the blocker is that nixpacks resolves runtimes from the Nix + # package set, which lags upstream badly. These run so the gap stays visible + # and so we notice the day nixpacks catches up — but they do not block a + # merge, because the fault is not in the example. + # + # Reasons, from the first CI run on 2026-08-01: + # 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` — no toolchain on PATH + # 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 command + nixpacks-gaps: + name: "${{ matrix.example }} (known gap)" + runs-on: ubuntu-latest + timeout-minutes: 30 + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - { example: examples/starters/go/net-http, port: 3000 } + - { example: examples/starters/go/gin, port: 3000 } + - { example: examples/starters/dotnet/web, port: 8080 } + - { example: examples/starters/astro, port: 4321 } + - { example: examples/starters/nuxt, port: 3000 } + - { example: examples/starters/swift/vapor, port: 8080 } + - { example: examples/starters/deno, port: 3000 } + - { example: examples/starters/java/spring-boot, port: 8080 } + - { example: examples/starters/bun/bun-server, port: 3000 } + - { example: examples/starters/bun/elysia, port: 3000 } + - { example: examples/starters/nodejs/hono, port: 3000 } + - { example: examples/starters/python/django, port: 8000 } + - { example: examples/starters/ruby/rails, 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 }} 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..06b2a77 --- /dev/null +++ b/examples/starters/README.md @@ -0,0 +1,63 @@ +# 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 groups. The first is blocking. The second, +`nixpacks-gaps`, is **not**: those starters are correct and build fine with a +current toolchain, but 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/nixpacks.toml b/examples/starters/dotnet/web/nixpacks.toml new file mode 100644 index 0000000..7729af0 --- /dev/null +++ b/examples/starters/dotnet/web/nixpacks.toml @@ -0,0 +1,2 @@ +[start] +cmd = "dotnet 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 @@ + 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/nixpacks.toml b/examples/starters/python/django/nixpacks.toml new file mode 100644 index 0000000..356a4a2 --- /dev/null +++ b/examples/starters/python/django/nixpacks.toml @@ -0,0 +1,5 @@ +[start] +cmd = "python manage.py migrate --run-syncdb && gunicorn mysite.wsgi --bind 0.0.0.0:${PORT:-8000}" + +[phases.setup] +aptPkgs = ["libpq-dev"] 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/ruby/rails/nixpacks.toml b/examples/starters/ruby/rails/nixpacks.toml new file mode 100644 index 0000000..ffafb58 --- /dev/null +++ b/examples/starters/ruby/rails/nixpacks.toml @@ -0,0 +1,5 @@ +[start] +cmd = "rails server -b 0.0.0.0 -p ${PORT:-3000}" + +[phases.setup] +aptPkgs = ["libpq-dev", "default-libmysqlclient-dev", "pkg-config"] 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()], +}); From b879d3de58cfa0cf6859e21b3ac3cbefc9471bad Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 1 Aug 2026 21:54:54 +0200 Subject: [PATCH 2/3] ci: report nixpacks gaps in one green job instead of 13 red ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit continue-on-error stops a job blocking a merge but does not stop GitHub painting it red, so a PR that passed showed 13 failing checks. That trains people to ignore red, which defeats the point of having CI at all. The 13 known-gap starters are now probed by a single job that always exits zero and writes a table to the run summary. It also calls out any starter that has started building, since nixpacks catching up is the news worth acting on — that was invisible when the jobs were permanently red. --- .github/workflows/starters.yml | 113 +++++++++++++++++++++------------ examples/starters/README.md | 12 ++-- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/.github/workflows/starters.yml b/.github/workflows/starters.yml index 7795618..faf2fab 100644 --- a/.github/workflows/starters.yml +++ b/.github/workflows/starters.yml @@ -55,50 +55,83 @@ jobs: example: ${{ matrix.example }} port: ${{ matrix.port }} - # Examples that are correct but which nixpacks cannot currently build. + # Starters that are correct but which nixpacks cannot currently build. # - # Every one of these was confirmed to build and serve with a modern - # toolchain; the blocker is that nixpacks resolves runtimes from the Nix - # package set, which lags upstream badly. These run so the gap stays visible - # and so we notice the day nixpacks catches up — but they do not block a - # merge, because the fault is not in the example. + # 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. # - # Reasons, from the first CI run on 2026-08-01: - # 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` — no toolchain on PATH - # 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 command + # 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: "${{ matrix.example }} (known gap)" + name: Known nixpacks gaps (report only) runs-on: ubuntu-latest - timeout-minutes: 30 - continue-on-error: true - strategy: - fail-fast: false - matrix: - include: - - { example: examples/starters/go/net-http, port: 3000 } - - { example: examples/starters/go/gin, port: 3000 } - - { example: examples/starters/dotnet/web, port: 8080 } - - { example: examples/starters/astro, port: 4321 } - - { example: examples/starters/nuxt, port: 3000 } - - { example: examples/starters/swift/vapor, port: 8080 } - - { example: examples/starters/deno, port: 3000 } - - { example: examples/starters/java/spring-boot, port: 8080 } - - { example: examples/starters/bun/bun-server, port: 3000 } - - { example: examples/starters/bun/elysia, port: 3000 } - - { example: examples/starters/nodejs/hono, port: 3000 } - - { example: examples/starters/python/django, port: 8000 } - - { example: examples/starters/ruby/rails, port: 3000 } + timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - - uses: ./.github/actions/build-and-serve - with: - example: ${{ matrix.example }} - port: ${{ matrix.port }} + + - 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/examples/starters/README.md b/examples/starters/README.md index 06b2a77..79f9bb6 100644 --- a/examples/starters/README.md +++ b/examples/starters/README.md @@ -43,10 +43,14 @@ reachable once deployed. 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 groups. The first is blocking. The second, -`nixpacks-gaps`, is **not**: those starters are correct and build fine with a -current toolchain, but nixpacks resolves runtimes from the Nix package set, -which lags upstream. As of 2026-08-01: +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 | |---|---| From 53cd57fd303fde90e9068875584562212956dcbf Mon Sep 17 00:00:00 2001 From: David Viejo Date: Mon, 3 Aug 2026 16:34:29 +0200 Subject: [PATCH 3/3] fix(starters): drop the four nixpacks.toml files that pin broken start commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each of these pinned a start command that only ever worked because of how Nixpacks happened to lay out its image, and breaks under any other builder: ruby/rails `rails server` with no `bundle exec` — `rails` is not on PATH, so the container exits with `exec: rails: not found` python/django `manage.py migrate` against a project with no database configured — `settings.DATABASES is improperly configured` php/laravel `php artisan serve`, a development server; also pins `php8.3-pgsql`, which does not resolve on the runtime base dotnet/web `dotnet run`, which needs the SDK — runtime images carry only the ASP.NET runtime, so it fails with "command not found" All four start and serve correctly once the file is gone: the builder's own provider already emits `bundle exec rails server`, gunicorn, FrankenPHP and `dotnet .dll`. The three remaining files (flask, fastapi, php/vanilla) pin commands that are correct, and are kept deliberately — they are the CI coverage for reading a nixpacks.toml at all. --- examples/starters/dotnet/web/nixpacks.toml | 2 -- examples/starters/php/laravel/nixpacks.toml | 5 ----- examples/starters/python/django/nixpacks.toml | 5 ----- examples/starters/ruby/rails/nixpacks.toml | 5 ----- 4 files changed, 17 deletions(-) delete mode 100644 examples/starters/dotnet/web/nixpacks.toml delete mode 100644 examples/starters/php/laravel/nixpacks.toml delete mode 100644 examples/starters/python/django/nixpacks.toml delete mode 100644 examples/starters/ruby/rails/nixpacks.toml diff --git a/examples/starters/dotnet/web/nixpacks.toml b/examples/starters/dotnet/web/nixpacks.toml deleted file mode 100644 index 7729af0..0000000 --- a/examples/starters/dotnet/web/nixpacks.toml +++ /dev/null @@ -1,2 +0,0 @@ -[start] -cmd = "dotnet run" diff --git a/examples/starters/php/laravel/nixpacks.toml b/examples/starters/php/laravel/nixpacks.toml deleted file mode 100644 index 04f6788..0000000 --- a/examples/starters/php/laravel/nixpacks.toml +++ /dev/null @@ -1,5 +0,0 @@ -[start] -cmd = "php artisan serve --host=0.0.0.0 --port=${PORT:-8000}" - -[phases.setup] -aptPkgs = ["libpq-dev", "php8.3-pgsql"] diff --git a/examples/starters/python/django/nixpacks.toml b/examples/starters/python/django/nixpacks.toml deleted file mode 100644 index 356a4a2..0000000 --- a/examples/starters/python/django/nixpacks.toml +++ /dev/null @@ -1,5 +0,0 @@ -[start] -cmd = "python manage.py migrate --run-syncdb && gunicorn mysite.wsgi --bind 0.0.0.0:${PORT:-8000}" - -[phases.setup] -aptPkgs = ["libpq-dev"] diff --git a/examples/starters/ruby/rails/nixpacks.toml b/examples/starters/ruby/rails/nixpacks.toml deleted file mode 100644 index ffafb58..0000000 --- a/examples/starters/ruby/rails/nixpacks.toml +++ /dev/null @@ -1,5 +0,0 @@ -[start] -cmd = "rails server -b 0.0.0.0 -p ${PORT:-3000}" - -[phases.setup] -aptPkgs = ["libpq-dev", "default-libmysqlclient-dev", "pkg-config"]