Your cron job said it worked. Did it?
Data syncs as TypeScript code. A bad result gets held back, not shipped.

One source changed a field. Puppi held that result back. Everything else kept running.
01
Catch the broken sync before your customers do.
Every result is checked against its schema, plus any rules you write in TypeScript. If a check fails, Puppi holds the result back. The last good version keeps serving.
02
Let Claude Code write it. Puppi checks it.
Pipelines are TypeScript files in your repo, so Cursor or Claude Code can read and fix them. No Python. No Docker. No Kubernetes. Ship with puppi deploy --env prod. Roll back to the last good version with one command.
1import { model, source } from "@puppi/sdk";23const schema = {4columns: [5{ name: "id", type: "string" },6{ name: "name", type: "string" },7{ name: "email", type: "string" },8],9};1011export const companies = source({12key: "hubspot/companies",13schema,14run: async (ctx) => {15await ctx.emit([{ id: "1", name: "Acme", email: "ada@acme.com" }]);16},17});1819export const customer = model({20key: "core/customer",21schema,22checks: {23custom: {24email_present: {25severity: "block",26run: ({ rows }) => ({27result: rows.every((row) => row.email != null && row.email !== "")28? "pass"29: "fail",30detail: "a customer row is missing email",31}),32},33},34},35run: async (ctx) => {36const up = ctx.input("hubspot/companies");37const { rows } = await ctx.query(`select id, name, email from ${up}`);38await ctx.emit(rows);39},40});
$ puppi deploy --env prod typecheck 3 assets ok bundle dist/puppi.js upload dep_8f3a21c0 pointer prod → dep_8f3a21c0 Active.
03
Sync the APIs you already use.
Pull from these, or from any REST API. Runs on app.puppi.dev, so you don't run any servers.
- Postgres
- Stripe
- Salesforce
- HubSpot
- BigQuery
- Snowflake
Build it with us.
We're working hands-on with a few TypeScript teams on Vercel, Fly or Render, with no DevOps hire. We'll get your first pipeline running with you, on a call.