{"id":48114992,"url":"https://github.com/joao-coimbra/topiq","last_synced_at":"2026-04-04T16:14:41.952Z","repository":{"id":346034273,"uuid":"1188313511","full_name":"joao-coimbra/topiq","owner":"joao-coimbra","description":"Type-safe MQTT client for TypeScript — define topics with Zod schemas and get fully typed publish, subscribe, and streaming","archived":false,"fork":false,"pushed_at":"2026-03-31T14:00:32.000Z","size":89,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-31T14:07:15.945Z","etag":null,"topics":["bun","iot","mqtt","typescript","zod"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/topiq","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joao-coimbra.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-21T22:46:54.000Z","updated_at":"2026-03-31T14:00:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/joao-coimbra/topiq","commit_stats":null,"previous_names":["joao-coimbra/topiq"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/joao-coimbra/topiq","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ftopiq","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ftopiq/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ftopiq/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ftopiq/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joao-coimbra","download_url":"https://codeload.github.com/joao-coimbra/topiq/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ftopiq/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31405699,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","iot","mqtt","typescript","zod"],"created_at":"2026-04-04T16:14:41.852Z","updated_at":"2026-04-04T16:14:41.918Z","avatar_url":"https://github.com/joao-coimbra.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cbr /\u003e\n\n# Topiq\n\n### Type-safe MQTT for TypeScript.\n\nDefine topics once with a Zod schema — get fully typed publish, subscribe, and streaming out of the box.\n\n\u003cbr /\u003e\n\n[![npm version](https://img.shields.io/npm/v/topiq?style=for-the-badge\u0026logo=npm\u0026color=CB3837\u0026logoColor=white)](https://www.npmjs.com/package/topiq)\n[![license](https://img.shields.io/badge/license-MIT-22C55E?style=for-the-badge)](./LICENSE)\n[![typescript](https://img.shields.io/badge/TypeScript-5-3178C6?style=for-the-badge\u0026logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![bun](https://img.shields.io/badge/Bun-ready-F9F1E1?style=for-the-badge\u0026logo=bun\u0026logoColor=black)](https://bun.sh)\n\n\u003cbr /\u003e\n\n[Quick Start](#install) · [Why Topiq](#why-topiq) · [Usage](#usage) · [API](#api) · [Contributing](#contributing)\n\n\u003cbr /\u003e\n\n\u003c/div\u003e\n\n---\n\n## Why Topiq?\n\nMQTT libraries give you raw strings and untyped buffers. You end up parsing, validating, and casting payloads by hand — everywhere. Topiq flips that: you declare your topics with a schema once, and every `.on()`, `.emit()`, and `.stream()` call is fully typed and validated automatically.\n\n```ts\n// ❌ Before — string topics, untyped buffers, manual parsing\nclient.subscribe('devices/+/status')\nclient.on('message', (topic, payload) =\u003e {\n  const data = JSON.parse(payload.toString()) // unknown, no validation\n})\n\n// ✅ After — typed topics, validated payloads, zero boilerplate\nconst client = topiq(config, { topics: { deviceStatus } })\n\nclient.on(deviceStatus, (data, { topic, params }) =\u003e {\n  console.log(data.online)      // boolean — inferred from your Zod schema\n  console.log(params.deviceId)  // string — extracted from the MQTT topic\n})\n```\n\n---\n\n## Features\n\n| | |\n|---|---|\n| **Type-safe topics** | Define topics with Express-style path params — TypeScript infers everything |\n| **Schema validation** | Every payload is validated against a Zod schema before reaching your handler |\n| **Path params** | `/devices/:deviceId/status` → automatically extracted as `{ deviceId: string }` |\n| **Async streaming** | Consume messages as an `AsyncIterable` with `AbortSignal` support |\n| **TLS support** | Pass `tls: true` for automatic MQTTS, or provide your own certificates |\n| **Dual factory API** | Pass a flat config or `{ client, topics }` — both are fully typed |\n\n---\n\n## Install\n\n```bash\nbun add topiq\n# or\nnpm install topiq\n```\n\n---\n\n## Agent Skill\n\nAn agent skill is available to give AI coding assistants full knowledge of the topiq API — topics, client config, pub/sub, streaming, errors, and types.\n\n```bash\nbun x skills add joao-coimbra/topiq\n```\n\nOnce installed, your AI assistant will automatically apply topiq patterns when working in a project that uses it.\n\n---\n\n## Usage\n\n### Define your topics\n\n```ts\nimport { topic } from 'topiq'\nimport { z } from 'zod'\n\nconst deviceStatus = topic('devices/:deviceId/status', z.object({\n  online: z.boolean(),\n  battery: z.number(),\n}))\n\nconst telemetry = topic('devices/:deviceId/telemetry', z.object({\n  temperature: z.number(),\n  humidity: z.number(),\n}))\n```\n\n### Create a client\n\n```ts\nimport { topiq } from 'topiq'\n\nconst client = topiq(\n  { host: 'broker.example.com', port: 1883 },\n  { topics: { deviceStatus, telemetry } }\n)\n```\n\nOr pass a single config object:\n\n```ts\nconst client = topiq({\n  client: { url: 'mqtt://broker.example.com:1883' },\n  topics: { deviceStatus, telemetry },\n})\n```\n\n### Subscribe — `on()`\n\n```ts\nconst unsubscribe = client.on(deviceStatus, (data, { topic, params }) =\u003e {\n  // data is fully typed: { online: boolean, battery: number }\n  console.log(data.online, data.battery)\n  console.log(params.deviceId) // e.g. \"abc-123\"\n  console.log(topic)           // e.g. \"devices/abc-123/status\"\n})\n\nunsubscribe() // remove the handler when done\n```\n\n### Publish — `emit()`\n\nUse `.build()` to construct the concrete topic string before publishing:\n\n```ts\nclient.emit(\n  deviceStatus.build({ deviceId: 'abc-123' }),\n  { online: true, battery: 87 }\n)\n```\n\nOr pass the topic string directly if you already have it:\n\n```ts\nclient.emit('devices/abc-123/status', { online: true, battery: 87 })\n```\n\n### Stream — `stream()`\n\nConsume messages as an async iterable. Pass an `AbortSignal` to stop the stream:\n\n```ts\nconst controller = new AbortController()\n\nfor await (const { data, topic } of client.stream(telemetry, controller.signal)) {\n  console.log(data.temperature, data.humidity)\n  console.log(topic) // e.g. \"devices/abc-123/telemetry\"\n}\n\n// call controller.abort() from outside this loop to stop the stream early\n```\n\n### TLS\n\n```ts\n// Automatic — switches to mqtts:// and port 8883\nconst client = topiq({ host: 'broker.example.com', tls: true }, { topics })\n\n// With certificates\nconst client = topiq({\n  host: 'broker.example.com',\n  tls: {\n    ca: await Bun.file('ca.crt').text(),\n    key: await Bun.file('client.key').text(),\n    cert: await Bun.file('client.crt').text(),\n  },\n}, { topics })\n```\n\n---\n\n## API\n\n### `topic(path, schema)`\n\nCreates a typed topic definition.\n\n```ts\nconst deviceStatus = topic('devices/:deviceId/status', z.object({\n  online: z.boolean(),\n}))\n```\n\n| Property / Method | Description |\n|---|---|\n| `.topic` | MQTT wildcard pattern, e.g. `\"devices/+/status\"` |\n| `.schema` | The Zod schema instance |\n| `.build(params)` | Builds a concrete topic string, e.g. `\"devices/abc-123/status\"`. Throws `MissingParamError` if a param is missing. |\n| `.extractParams(mqttTopic)` | Extracts path params from a live MQTT topic string. Throws `TopicPatternMismatchError` if the topic doesn't match. |\n\n---\n\n### `topiq(client, options)` / `topiq(config)`\n\nCreates a `TopiqClient`.\n\n**Client config — provide either `url` or `host`, not both:**\n\nBy URL:\n\n| Field | Type | Required | Example |\n|---|---|---|---|\n| `url` | `string` | ✓ | `\"mqtt://broker.example.com:1883\"` |\n| `tls` | `true \\| TLSConfig` | | `true` |\n| `username` | `string` | | |\n| `password` | `string` | | |\n\nBy host:\n\n| Field | Type | Required | Example |\n|---|---|---|---|\n| `host` | `string` | ✓ | `\"broker.example.com\"` |\n| `port` | `number` | | `1883` |\n| `protocol` | `string` | | `\"mqtt\"`, `\"mqtts\"` |\n| `tls` | `true \\| TLSConfig` | | `true` |\n| `username` | `string` | | |\n| `password` | `string` | | |\n\n---\n\n### `TopiqClient`\n\n| Method | Description |\n|---|---|\n| `.on(topic, (data, { topic, params }) =\u003e void)` | Subscribe and receive validated payloads. Returns an unsubscribe function. |\n| `.emit(concreteTopic, data)` | Publish a typed payload to a concrete topic string. |\n| `.stream(topic, signal?)` | Returns an `AsyncIterable\u003c{ data, topic }\u003e`. |\n| `.ready(timeout?)` | Resolves when connected. Rejects after `timeout` ms (default: 1000). |\n| `.disconnect()` | Close the MQTT connection. |\n| `.isConnected` | `true` when the underlying client is connected. |\n\n---\n\n### Errors\n\n```ts\nimport {\n  MissingParamError,\n  TopicPatternMismatchError,\n  TopicValidationError,\n  UnregisteredTopicError,\n} from 'topiq/errors'\n```\n\n| Error | Description |\n|---|---|\n| `TopicValidationError` | Payload failed Zod schema validation |\n| `TopicPatternMismatchError` | MQTT topic string doesn't match the registered pattern |\n| `MissingParamError` | A required path param was missing from a `.build()` call |\n| `UnregisteredTopicError` | A topic pattern is not registered with the client |\n\n---\n\n## Architecture\n\n```\nsrc/\n├── topiq.ts          # TopiqClient class and topiq() factory\n├── topic.ts          # Topic class and topic() factory\n├── types/\n│   ├── topic-pattern.ts    # TopicPattern\u003cT\u003e — Express path → MQTT wildcard\n│   └── extract-params.ts   # ExtractParams\u003cT\u003e — typed path param extraction\n└── errors/\n    ├── missing-param.error.ts\n    ├── topic-pattern-mismatch.error.ts\n    ├── topic-validation.error.ts\n    └── unregistered-topic.error.ts\n```\n\nTest infrastructure lives in `test/factories/` and `test/helpers/` — not co-located with source.\n\n---\n\n## Development\n\n**Requirements:** Bun \u003e= 1.0, Docker (for E2E tests)\n\n```bash\nbun install          # install dependencies\nbun test             # run unit tests\nbun x ultracite fix  # lint + format\n```\n\n### Testing\n\n```bash\nbun test             # unit tests (no external dependencies)\nbun run test:e2e     # e2e tests — spins up a Mosquitto broker via Docker\n```\n\nUnit tests live alongside source as `*.spec.ts`. E2E tests are `*.e2e-spec.ts` and run against a real Mosquitto 2 broker managed by Docker Compose.\n\n### CI\n\nEvery pull request runs three parallel jobs via GitHub Actions:\n\n| Job | What it checks |\n|---|---|\n| `lint` | Biome via Ultracite (`bun x ultracite check`) |\n| `test` | Unit tests (`bun test`) |\n| `e2e` | Integration tests against a real MQTT broker |\n\nReleases are published to npm automatically when a `v*.*.*` tag is pushed, using [OIDC Trusted Publisher](https://docs.npmjs.com/generating-provenance-statements) — no long-lived token stored in secrets.\n\n---\n\n## Contributing\n\nBug reports and feature requests are welcome via [GitHub Issues](https://github.com/joao-coimbra/topiq/issues). For significant features, open a [Discussion](https://github.com/joao-coimbra/topiq/discussions) first.\n\n```bash\nbun install          # setup\nbun test             # make sure everything passes\nbun x ultracite fix  # format before committing\n```\n\nCommits follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.). Pull requests are squash-merged.\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) for community standards.\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n**Built with ❤️ for the TypeScript community.**\n\n[Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [MIT License](./LICENSE)\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoao-coimbra%2Ftopiq","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoao-coimbra%2Ftopiq","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoao-coimbra%2Ftopiq/lists"}