Starter project
A two-endpoint Joule Cloud app you can clone, deploy, and use as a template. Built in TypeScript with the OpenAI SDK and a Postgres-shaped DB.
What it does
One service, two routes:
POST /ask— takes a question, calls Inference, stores the Q+A+joules in the database, returns the answer plus a per-request energy footprint.GET /history— returns the last 50 Q+A pairs with their joule totals.
Get the code
git clone https://git.openie.sh/openie/joule-starter.git
cd joule-starter
cp .env.example .env
# edit .env with your JC_API_KEY
docker build -t ghcr.io/me/joule-starter:1.0 .
docker push ghcr.io/me/joule-starter:1.0
jc db create joule-starter --region eu-fi --size 10GB
invisible deploy
What's in the repo
joule-starter/
├── Dockerfile # multi-stage Node 22 build, < 50 MB image
├── invisible.hcl # workload + db + route declaration
├── .env.example # JC_API_KEY, DATABASE_URL
├── .github/workflows/
│ └── deploy.yml # CI: build → push → invisible deploy
├── src/
│ ├── server.ts # Fastify app, two routes
│ ├── db.ts # sqlx-like Postgres client
│ └── joule-client.ts # OpenAI SDK wrapper with header capture
├── migrations/
│ └── 001_init.sql # questions table
└── package.json
The handler shape
// src/server.ts
import Fastify from "fastify";
import { askJoule } from "./joule-client.js";
import { saveAnswer, recentAnswers } from "./db.js";
const app = Fastify({ logger: true });
app.post("/ask", async (req) => {
const { question } = req.body as { question: string };
const { answer, joules, tier } = await askJoule(question);
await saveAnswer({ question, answer, joules, tier });
return { answer, joules, tier };
});
app.get("/history", async () => {
return await recentAnswers(50);
});
app.listen({ port: 3000, host: "0.0.0.0" });
The joule-aware client
// src/joule-client.ts
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.greenjoules.cloud/v1",
apiKey: process.env.JC_API_KEY!,
});
export async function askJoule(question: string) {
const resp = await client.chat.completions
.with_raw_response
.create({
model: "auto",
messages: [
{ role: "system", content: "You are a concise assistant." },
{ role: "user", content: question },
],
});
const data = resp.parse();
return {
answer: data.choices[0].message.content ?? "",
joules: parseFloat(resp.headers.get("x-energy-joules") ?? "0"),
tier: resp.headers.get("x-tier") ?? "",
};
}
The schema
-- migrations/001_init.sql
CREATE TABLE IF NOT EXISTS questions (
id BIGSERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
joules NUMERIC(10, 4) NOT NULL,
tier TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX questions_created_idx ON questions (created_at DESC);
The deploy file
# invisible.hcl
workload "joule-starter" {
image = "ghcr.io/me/joule-starter:1.0"
region = "eu-fi"
cpu = "1 vCPU"
memory = "512 MB"
scale = { min = 0, max = 10 }
env = {
JC_API_KEY = "secret:JC_API_KEY"
DATABASE_URL = database.joule-starter.url
NODE_ENV = "production"
}
}
database "joule-starter" {
region = "eu-fi"
size_gb = 10
}
route "starter.example.com" {
to = workload.joule-starter
https = true
}
The CI step
# .github/workflows/deploy.yml
name: deploy
on: { push: { branches: [main] } }
jobs:
ship:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- run: |
sed -i "s|image *= .*|image = \"ghcr.io/${{ github.repository }}:${{ github.sha }}\"|" invisible.hcl
curl -L https://invisible.openie.sh/install.sh | sh
jc login --token "${{ secrets.JC_API_KEY }}"
invisible deploy --auto-approve
What to do with it
- Use as a scaffold for any "small service in front of Inference + a database" app you're building.
- Strip the
historyendpoint and swap the model from"auto"to a pinned one to bench specific shapes. - Add
X-Customer-Tagon each call (from a JWT, header, etc.) to enable per-customer attribution. - Add a webhook subscription on the
balance.lowevent so the starter pings Slack before the bill hits zero.
Source & license
MIT. PRs welcome. Repo at git.openie.sh/openie/joule-starter (mirror on GitHub at openIE-dev/joule-starter).
What to read next
If you want to add observability to the starter, jump to Energy receipts. If you want to push this shape to a fuller RAG app, jump to Build a RAG app. If you want to add Functions for cron + webhook handlers, jump to Functions.