{"_id":"@avvos/convoy","name":"@avvos/convoy","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@avvos/convoy","version":"0.0.1","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/hamzatekin/convoy.git"},"homepage":"https://github.com/hamzatekin/convoy#readme","bugs":{"url":"https://github.com/hamzatekin/convoy/issues"},"workspaces":["playground"],"bin":{"convoy":"bin/convoy.js"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"./node":{"types":"./dist/node.d.ts","default":"./dist/node.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=18.19"},"publishConfig":{"access":"public"},"lint-staged":{"*.{js,jsx,ts,tsx,json,css,md}":["prettier --write"]},"scripts":{"dev":"vite","build":"tsc -p tsconfig.build.json","preview":"vite preview","convoy:dev":"node bin/convoy.js dev","convoy:server:debug":"CONVOY_DEBUG=1 npm --prefix playground run convoy:server","playground:dev":"npm --prefix playground run dev","playground:build":"npm --prefix playground run build","playground:preview":"npm --prefix playground run preview","playground:typecheck":"npm --prefix playground run typecheck","playground:convoy:dev":"npm --prefix playground run convoy:dev","playground:convoy:once":"npm --prefix playground run convoy:once","playground:convoy:server":"npm --prefix playground run convoy:server","playground:convoy:server:debug":"CONVOY_DEBUG=1 npm --prefix playground run convoy:server","prepare":"husky","format":"prettier --write .","prepublishOnly":"npm run build","test":"vitest run","typecheck":"tsc --noEmit"},"dependencies":{"drizzle-orm":"^0.45.1","dotenv":"^17.2.3","pg":"^8.16.3","tsx":"^4.20.5"},"peerDependencies":{"zod":"^3.23.0 || ^4.0.0"},"devDependencies":{"@testing-library/react":"^16.3.1","@types/node":"^25.0.3","@types/pg":"^8.15.5","@types/react":"^19.1.13","happy-dom":"^18.0.0","husky":"^9.1.7","lint-staged":"^16.2.7","prettier":"^3.7.4","react":"^19.1.1","typescript":"~5.9.3","vite":"^7.2.4","vitest":"^4.0.16","zod":"^4.2.1"},"gitHead":"7d8b83296076eddd01f44b661dbe8515037c86bc","_id":"@avvos/convoy@0.0.1","description":"**Convex-style reactive backend DX — on your own Postgres.**","_nodeVersion":"24.11.1","_npmVersion":"11.6.4","dist":{"integrity":"sha512-n0d48XC5XF0dN0oBn3xQYBVvbJrzlb7zeo+46kHmcfOU0W7vmIapA0D9U66f1MKQYmSBUwgikP7CUa56tez1/Q==","shasum":"e0235fe4a9118189933bcf6a33953a3df98f2dc7","tarball":"https://registry.npmjs.org/@avvos/convoy/-/convoy-0.0.1.tgz","fileCount":36,"unpackedSize":159786,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC9ZZFGDS9vdSgNJ/l+uOttDe7VTLpMfMPT8UETdQoHgAIhANsChc9blD6PJ7WgoROkwUEdXKTo8oFSwlLIf+urZxql"}]},"_npmUser":{"name":"avvosmeyz","email":"hmztkn@gmail.com"},"directories":{},"maintainers":[{"name":"avvosmeyz","email":"hmztkn@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convoy_0.0.1_1767438818219_0.20007307911439076"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-03T11:13:38.154Z","0.0.1":"2026-01-03T11:13:38.365Z","modified":"2026-01-03T11:13:38.631Z"},"maintainers":[{"name":"avvosmeyz","email":"hmztkn@gmail.com"}],"description":"**Convex-style reactive backend DX — on your own Postgres.**","homepage":"https://github.com/hamzatekin/convoy#readme","repository":{"type":"git","url":"git+https://github.com/hamzatekin/convoy.git"},"bugs":{"url":"https://github.com/hamzatekin/convoy/issues"},"license":"MIT","readme":"# Convoy\n\n**Convex-style reactive backend DX — on your own Postgres.**\n\nConvoy is a self-hosted backend runtime where you build your backend by writing **queries and mutations**, and your UI stays in sync automatically via **server-pushed updates**.\n\nYou keep full ownership of your database. Convoy handles execution, typing, and reactivity.\n\nStart fast with **JSONB document tables**, iterate quickly, and keep a clear path toward stricter schemas and relational data as your product matures.\n\n**Status:** `0.0.x` — MVP (core ideas implemented, APIs still stabilizing)\n\n---\n\n## Why Convoy\n\n- **Postgres as the source of truth** (self-hosted, future-proof)\n- **JSONB-first schema** for fast iteration\n- **End-to-end TypeScript types** (schema → server → client)\n- **Reactive queries** with server-pushed updates\n- No manual cache invalidation or refetch logic\n- Clear path to stronger schemas as your product grows\n\n## Installation\n\n```bash\nnpm install @avvos/convoy zod\n```\n\nRequires Node `>=18.19` (or Bun) for the CLI.\n\n---\n\n## Core Concepts\n\n### Schema\n\nDefine your data shape once using Zod. Convoy stores rows as JSONB documents in Postgres and ensures tables and indexes exist during development.\n\n### Queries\n\nQueries are pure read functions. Clients subscribe to queries and receive live updates automatically.\n\n### Mutations\n\nMutations are write + business logic functions. When a mutation runs, Convoy invalidates affected queries and pushes updated results to subscribed clients.\n\n### Reactivity\n\nConvoy uses Postgres `LISTEN / NOTIFY` for change signals and Server-Sent Events (SSE) to stream authoritative query results to clients.\n\n## Mental model\n\n- Define tables + indexes once in `convoy/schema.ts`.\n- Write pure `query` functions and side-effecting `mutation` functions.\n- The server validates inputs, injects context (db + auth), and executes functions.\n- Clients call generated refs; `useQuery` stays subscribed and gets server-pushed updates.\n\n## Data flow (simplified)\n\n```\nClient useQuery  ->  /api/query/:name  ->  run query  ->  SSE stream\nClient mutation  ->  /api/mutation/:name  ->  write DB  ->  NOTIFY -> refresh SSE\n```\n\n## What Convoy is / is not\n\nConvoy is:\n\n- A typed function runtime (query/mutation) on top of your Postgres.\n- A reactive layer that keeps clients in sync via SSE.\n- A schema-first JSONB model optimized for iteration.\n\nConvoy is not:\n\n- A hosted backend or auth provider.\n- A replacement for all of your backend code (you can mix it).\n- A migration engine that drops/renames tables for you.\n\n## Comparison (short)\n\nvs REST:\n\n- Convoy gives end-to-end types and reactive subscriptions; REST gives full manual control.\n- Convoy hides routing; REST exposes explicit endpoints and verbs.\n\nvs Convex:\n\n- Convoy runs on your Postgres; Convex runs on hosted infra.\n- Convoy uses JSONB + SQL; Convex uses its own storage/engine.\n- Convoy is bring-your-own-auth; Convex provides hosted auth integrations.\n\n## Tradeoffs & limitations\n\n- SSE only (no WebSocket transport yet).\n- JSONB-first model\n- No destructive migrations (tables/indexes are created only).\n- Long-lived server process required (not serverless-friendly out of the box).\n\n## Quickstart\n\n### 1) Define your schema\n\nCreate `convoy/schema.ts` in your project root:\n\n```ts\n// convoy/schema.ts\nimport { defineSchema, defineTable, defineRef } from '@avvos/convoy';\nimport { z } from 'zod';\n\nexport const schema = defineSchema({\n  users: defineTable({\n    name: z.string(),\n    createdAt: z.number(),\n  }),\n  projects: defineTable({\n    name: z.string(),\n    userId: defineRef('users'),\n    createdAt: z.number(),\n  }).index('by_userId', ['userId']),\n});\n```\n\n### 2) Write queries + mutations\n\nCreate files under `convoy/functions`:\n\n```ts\n// convoy/functions/projects.ts\nimport { defineRef } from '@avvos/convoy';\nimport { mutation, query } from '../_generated/server';\nimport { z } from 'zod';\n\nexport const createProject = mutation({\n  input: { userId: defineRef('users'), name: z.string() },\n  handler: async (ctx, input) => {\n    return ctx.db.insert('projects', {\n      userId: input.userId,\n      name: input.name,\n      createdAt: Date.now(),\n    });\n  },\n});\n\nexport const listProjects = query({\n  input: { userId: defineRef('users') },\n  handler: async (ctx, input) => {\n    return ctx.db\n      .query('projects')\n      .withIndex('by_userId', (q) => q.eq('userId', input.userId))\n      .order('desc', 'createdAt')\n      .collect();\n  },\n});\n```\n\n### 3) Sync and generate API bindings\n\nRun the dev command (watches for changes by default, use `--once` for a single sync):\n\n```bash\nnpx @avvos/convoy dev\n```\n\nOther package managers:\n\n```bash\npnpm dlx @avvos/convoy dev\nyarn dlx @avvos/convoy dev\nbunx @avvos/convoy dev\n```\n\nFor local development of this repo, use `npm run convoy:dev` or `bun run convoy:dev` (bunx installs from the registry unless you add a local `file:` dependency).\n\nConvoy reads `DATABASE_URL` from `process.env`. If you install `dotenv`, the CLI will also load `.env` files automatically; otherwise set env vars yourself.\n\nThis will:\n\n- create the database if needed\n- create tables + JSONB indexes\n- generate `convoy/_generated/api.ts`, `convoy/_generated/functions.ts`, and `convoy/_generated/server.ts`\n- generate `convoy/_generated/http.ts` (HTTP + SSE subscriptions)\n- start the local Convoy HTTP server\n\nIf `convoy/server.ts` exists, its `createContext(req, base)` (and optional `configureServer`) is used automatically.\n\nProduction workflow (explicit, safe):\n\n```bash\nconvoy migrate\n```\n\n`convoy migrate` runs schema sync once, emits warnings for destructive or incompatible changes, and never drops tables or indexes. Use this in deploy pipelines (alias: `convoy deploy`).\n\n### 4) Use it on the client (React)\n\n```ts\nimport { skipToken, useMutation, useQuery } from '@avvos/convoy/react';\nimport { api } from '../convoy/_generated/api';\n\nconst createProject = useMutation(api.projects.createProject);\nconst { data, connectionState, isReconnecting, isStale } = useQuery(api.projects.listProjects, { userId });\nconst { data: tasks } = useQuery(api.tasks.listTasks, projectId ? { projectId } : skipToken);\n```\n\nDirect client usage (non-React)\n\n```ts\nimport { createConvoyClient } from '@avvos/convoy/client';\nimport { api } from '../convoy/_generated/api';\n\nconst client = createConvoyClient();\nawait client.mutation(api.projects.createProject, { userId, name: 'My App' });\n```\n\nStructured errors and mutation state:\n\n```ts\nimport { ConvoyError } from '@avvos/convoy/client';\nimport { useMutationState } from '@avvos/convoy/react';\n\nconst { mutate, isLoading, error } = useMutationState(api.projects.createProject);\n\ntry {\n  await mutate({ userId, name: 'My App' });\n} catch (err) {\n  if (err instanceof ConvoyError) {\n    console.log(err.code, err.message);\n  }\n}\n```\n\n---\n\n## Auth via request context\n\nConvoy treats auth as **request-scoped data on your context**. Export `createContext(req, base)` from `convoy/server.ts` and `convoy dev` will pick it up automatically.\n\nIf you build a custom server entry, use `createBaseContext(db)` to assemble the base context and then extend it in your request context.\n\n```ts\n// convoy/server.ts\nimport type { IncomingMessage } from 'node:http';\nimport type { ServerContext } from './_generated/server';\nimport { convoyError } from '@avvos/convoy';\n\nexport async function createContext(req: IncomingMessage, base: ServerContext) {\n  const token = req.headers.authorization?.replace(/^Bearer /, '');\n  if (!token) {\n    throw convoyError('UNAUTHORIZED', 'Missing token');\n  }\n  const user = await verifyJwt(token);\n  return { ...base, auth: { userId: user.sub } };\n}\n```\n\nCookie session example:\n\n```ts\nexport async function createContext(req: IncomingMessage, base: ServerContext) {\n  const cookie = req.headers.cookie ?? '';\n  const sessionId = cookie.split('session=')[1]?.split(';')[0];\n  if (!sessionId) {\n    throw convoyError('UNAUTHORIZED', 'Missing session');\n  }\n  const session = await loadSession(sessionId);\n  return { ...base, auth: { userId: session.userId } };\n}\n```\n\nOptional server hook:\n\n```ts\nexport function configureServer({ server }) {\n  server.on('request', (_req, _res) => {\n    // add custom logging or headers\n  });\n}\n```\n\nBest DX pattern (recommended):\n\n1. Default: generated server entry (zero config). CLI generates `convoy/_generated/http.ts` with `createBaseContext(db)` wired in, and `npx @avvos/convoy dev` just works.\n2. Optional: user-defined server entry (advanced). Create `convoy/server.ts` and export `createContext(req, base)` (and optionally `configureServer`); the CLI auto-detects it and uses it.\n\nBest practices:\n\n- Resolve auth once per request (or once per SSE subscription connection) and attach it to context.\n- Throw `convoyError('UNAUTHORIZED', ...)` or `convoyError('FORBIDDEN', ...)` to return structured errors.\n- If you use header-based auth, note that SSE cannot send custom headers; prefer cookie sessions or set `subscribe: false`.\n- Bring your own auth — Convoy does not require a hosted auth provider.\n\nTyped auth helpers:\n\n```ts\n// convoy/functions/_auth.ts\nimport { convoyError, createFunctionHelpers, type Id } from '@avvos/convoy';\nimport type { ServerContext } from '../_generated/server';\n\nexport type AuthContext = ServerContext & { auth: { userId: Id<'users'> } | null };\n\nconst helpers = createFunctionHelpers<AuthContext>();\nexport const authQuery = helpers.query;\nexport const authMutation = helpers.mutation;\n\nexport function requireAuth(ctx: AuthContext) {\n  if (!ctx.auth?.userId) {\n    throw convoyError('UNAUTHORIZED', 'Missing session');\n  }\n  return ctx.auth;\n}\n```\n\n---\n\n### How reactivity works (high level)\n\n1. useQuery opens an SSE subscription\n2. The server runs the query and streams the initial result\n3. A mutation runs and writes to Postgres\n4. Postgres emits NOTIFY\n5. The server refreshes affected subscriptions\n6. Updated query results are pushed to clients\n\nThe server is always the source of truth.\n\n### Escape hatches\n\nRaw SQL:\n\n```ts\nimport { sql } from 'drizzle-orm';\n\nconst rows = await ctx.db.raw<{ total: number }>(sql`select count(*) as total from users`);\n```\n\nOpt out of table management:\n\n```ts\nimport { defineSchema, defineTable } from '@avvos/convoy';\nimport { z } from 'zod';\n\nexport default defineSchema({\n  audit_log: defineTable({ event: z.string() }).unmanaged(),\n});\n```\n\nMixing Convoy with traditional backends:\n\n- Use Convoy for realtime slices (collab, dashboards) while keeping REST/GraphQL for everything else.\n- Point both systems at the same database; Convoy never drops tables and only creates what it manages.\n- You can call existing services from Convoy functions (e.g. via HTTP or shared modules).\n\nEject story:\n\n- Your data stays in Postgres; you can stop the Convoy server without losing any rows.\n- Queries and mutations are just TypeScript functions — move them into another backend or reuse them in APIs.\n- You can keep generated types (`convoy/_generated`) or replace them with your own client logic.\n\n### Roadmap (high level)\n\nNear-term (v1):\n\n- runtime hardening and reconnect guarantees\n- structured error handling\n- auth patterns via request context\n- clear dev vs deploy workflows\n- escape hatches (raw SQL, interop)\n\n---\n\nLicense\n\nMIT\n","readmeFilename":"README.md","_rev":"1-c170a210aa963c4d68fa1a1a19911d80"}