Skip to content

Commit 46426b6

Browse files
committed
feat: initialize mqttkit
0 parents  commit 46426b6

53 files changed

Lines changed: 4179 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
.vite/
4+
.DS_Store
5+
legacy

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 keyp-dev
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
# mqttkit
2+
3+
[简体中文](README.zh-CN.md)
4+
5+
Build MQTT applications with an Elysia-like API: compose broker adapters, ordered middleware, topic routers, authentication, lifecycle events, and business services with `new MqttApp().use(...).use(router(...))`.
6+
7+
mqttkit does not reimplement the MQTT protocol. A broker such as Aedes owns CONNECT, SUBSCRIBE, PUBLISH, QoS, retain, sessions, persistence, and MQTT-over-WebSocket. mqttkit adds an application framework layer on top.
8+
9+
## Features
10+
11+
- Ordered `use()` middleware for auth, logging, audit, validation, and interception.
12+
- `router().topic()` declares MQTT topic routes, publish policy, and subscribe policy.
13+
- `ctx.params` extracts topic params such as `devices/:uid/events`.
14+
- `ctx.services` injects Kafka, Redis, database, audit, or domain services.
15+
- `app.on()` observes broker lifecycle events such as client, publish, subscribe, ack, and errors.
16+
- `app.publish()` lets services, workers, and consumers push messages to MQTT clients through the broker.
17+
- `@mqttkit/aedes` provides TCP MQTT and MQTT-over-WebSocket support through Aedes.
18+
- Built for Bun and TypeScript.
19+
20+
## Installation
21+
22+
```bash
23+
bun add @mqttkit/core @mqttkit/aedes aedes
24+
```
25+
26+
Install only `@mqttkit/core` if you only need the core types, router, or a custom broker adapter.
27+
28+
## Quick Start
29+
30+
```ts
31+
import { aedes } from '@mqttkit/aedes'
32+
import { MqttApp, router } from '@mqttkit/core'
33+
34+
type Services = {
35+
audit: {
36+
log(event: string, fields: Record<string, unknown>): Promise<void>
37+
}
38+
}
39+
40+
const app = new MqttApp<{ principal?: { uid: string }; services: Services }>()
41+
.decorate('audit', {
42+
async log(event, fields) {
43+
console.log(event, fields)
44+
},
45+
})
46+
.use(
47+
aedes({
48+
tcp: { port: 1883 },
49+
ws: { port: 8888, path: '/mqtt' },
50+
authenticate({ clientId, username }) {
51+
if (!username) return false
52+
return { uid: username || clientId }
53+
},
54+
}),
55+
)
56+
.use(async (ctx, next) => {
57+
await ctx.services.audit.log('mqtt.message', {
58+
clientId: ctx.clientId,
59+
topic: ctx.topic,
60+
})
61+
await next()
62+
})
63+
.use(
64+
router<{ principal?: { uid: string }; services: Services }>()
65+
.topic('devices/:uid/events', {
66+
publish: ({ params, principal }) => params.uid === principal?.uid,
67+
async onMessage(ctx) {
68+
await ctx.publish(`server/${ctx.params.uid}/echo`, ctx.payload, { qos: 0 })
69+
},
70+
})
71+
.topic('server/:uid/echo', {
72+
subscribe: ({ params, principal }) => params.uid === principal?.uid,
73+
}),
74+
)
75+
76+
await app.listen()
77+
```
78+
79+
Clients connect with standard MQTT TCP or MQTT-over-WebSocket:
80+
81+
```ts
82+
import mqtt from 'mqtt'
83+
84+
const client = mqtt.connect('ws://localhost:8888/mqtt', {
85+
username: 'demo',
86+
})
87+
88+
client.subscribe('server/demo/echo')
89+
client.publish('devices/demo/events', JSON.stringify({ temperature: 22 }), { qos: 0 })
90+
```
91+
92+
## Router
93+
94+
MQTT has topics, not HTTP methods. mqttkit uses `topic(pattern, config)` to declare a route, publish policy, subscribe policy, and optional inbound message handler.
95+
96+
```ts
97+
router()
98+
.topic('devices/:uid/events', {
99+
publish: ({ params, principal }) => params.uid === principal?.uid,
100+
subscribe: false,
101+
async onMessage(ctx) {
102+
console.log(ctx.params.uid, ctx.payload.toString())
103+
},
104+
})
105+
.topic('server/:uid/commands', {
106+
publish: false,
107+
subscribe: ({ params, principal }) => params.uid === principal?.uid,
108+
})
109+
```
110+
111+
Default policies:
112+
113+
- A topic with `onMessage` defaults to `publish: true` and `subscribe: false`.
114+
- A topic without `onMessage` defaults to `publish: false` and `subscribe: true`.
115+
116+
## Middleware
117+
118+
`use()` runs in registration order. If middleware does not call `next()`, the remaining middleware and route handler do not run.
119+
120+
```ts
121+
const app = new MqttApp()
122+
.use(async (ctx, next) => {
123+
console.log('[mqtt]', ctx.clientId, ctx.topic)
124+
await next()
125+
})
126+
.use(
127+
router()
128+
.use(async (ctx, next) => {
129+
if (ctx.params.uid === 'blocked') return
130+
await next()
131+
})
132+
.topic('devices/:uid/events', {
133+
onMessage(ctx) {
134+
console.log(ctx.payload.toString())
135+
},
136+
}),
137+
)
138+
```
139+
140+
Pipeline:
141+
142+
```text
143+
app middleware -> route middleware -> onMessage
144+
```
145+
146+
## Events
147+
148+
`app.on()` receives broker lifecycle events. The Aedes adapter currently forwards:
149+
150+
```ts
151+
import type { MqttEventName } from '@mqttkit/core'
152+
153+
const eventNames: MqttEventName[] = [
154+
'client',
155+
'clientReady',
156+
'clientDisconnect',
157+
'keepaliveTimeout',
158+
'clientError',
159+
'connectionError',
160+
'connackSent',
161+
'ping',
162+
'publish',
163+
'ack',
164+
'subscribe',
165+
'unsubscribe',
166+
]
167+
168+
for (const eventName of eventNames) {
169+
app.on(eventName, (event) => {
170+
console.log(event.type, event.clientId, event.topic)
171+
})
172+
}
173+
```
174+
175+
ACK is produced by the broker according to MQTT packet lifecycle. mqttkit does not define a custom JSON ack.
176+
177+
## Service Push
178+
179+
Any external service, timer, worker, queue consumer, or Kafka consumer can call `app.publish()` to send messages into the broker and let the broker deliver them to subscribed MQTT clients.
180+
181+
```text
182+
external service / worker / consumer
183+
-> app.publish(topic, payload, options)
184+
-> @mqttkit/aedes adapter
185+
-> Aedes broker
186+
-> subscribed MQTT clients
187+
```
188+
189+
Declare a server-owned topic that clients may subscribe to but may not publish to:
190+
191+
```ts
192+
const app = new MqttApp()
193+
.use(aedes({ tcp: { port: 1886 } }))
194+
.use(
195+
router().topic('users/:uid/notifications', {
196+
subscribe: true,
197+
publish: false,
198+
}),
199+
)
200+
201+
billing.onInvoicePaid(async (event) => {
202+
await app.publish(`users/${event.uid}/notifications`, event.payload, { qos: 1 })
203+
})
204+
```
205+
206+
## Kafka Bridge
207+
208+
Kafka, databases, and other services are injected with `decorate()`. MQTT -> Kafka happens inside `onMessage`; Kafka -> MQTT happens when a consumer calls `app.publish()`.
209+
210+
```ts
211+
type Kafka = {
212+
produce(topic: string, value: Buffer, key: string): Promise<void>
213+
onCommands(handler: (message: { key: string; value: Buffer }) => Promise<void>): void
214+
}
215+
216+
const app = new MqttApp<{ services: { kafka: Kafka } }>()
217+
.decorate('kafka', kafka)
218+
.use(aedes({ tcp: { port: 1883 } }))
219+
.use(
220+
router<{ services: { kafka: Kafka } }>()
221+
.topic('devices/:uid/events', {
222+
async onMessage(ctx) {
223+
await ctx.services.kafka.produce('device.events', ctx.payload, ctx.params.uid)
224+
},
225+
})
226+
.topic('server/:uid/commands'),
227+
)
228+
229+
kafka.onCommands(async (message) => {
230+
await app.publish(`server/${message.key}/commands`, message.value, { qos: 1 })
231+
})
232+
```
233+
234+
The client only needs a normal MQTT subscription:
235+
236+
```ts
237+
const client = mqtt.connect('mqtt://localhost:1885')
238+
client.subscribe('server/demo/commands', { qos: 1 })
239+
client.on('message', (topic, payload) => {
240+
console.log(topic, payload.toString())
241+
})
242+
```
243+
244+
## Aedes Integration
245+
246+
`@mqttkit/aedes` supports:
247+
248+
- Creating an Aedes instance for mqttkit.
249+
- Using an externally created Aedes instance.
250+
- Starting a TCP MQTT server.
251+
- Starting a standard MQTT-over-WebSocket server.
252+
- Passing Aedes `persistence` through.
253+
- Forwarding Aedes lifecycle events to `app.on()`.
254+
- Returning `principal` from `authenticate` and injecting it into policies and handler context.
255+
- Delegating publish / subscribe authorization to mqttkit route policies.
256+
257+
```ts
258+
import { aedes } from '@mqttkit/aedes'
259+
260+
new MqttApp()
261+
.use(
262+
aedes({
263+
tcp: { port: 1883 },
264+
ws: { port: 8888, path: '/mqtt' },
265+
authenticate({ username }) {
266+
if (!username) return false
267+
return { uid: username }
268+
},
269+
}),
270+
)
271+
```
272+
273+
Use Aedes persistence adapters for offline queues, retain, QoS sessions, and durable storage. mqttkit only owns the application layer.
274+
275+
## Examples
276+
277+
- `examples/aedes-basic`: TCP broker, authentication, middleware, topic routes, and server-side publish.
278+
- `examples/aedes-ws`: standard MQTT-over-WebSocket; mqtt.js connects to `ws://localhost:8888/mqtt`.
279+
- `examples/events`: broker lifecycle events.
280+
- `examples/service-push`: external service calls `app.publish()`, then Aedes delivers to subscribed MQTT clients.
281+
- `examples/kafka-bridge`: MQTT messages go to Kafka, and Kafka consumer messages are forwarded to MQTT clients.
282+
283+
Run an example:
284+
285+
```bash
286+
bun install
287+
bun run --cwd examples/aedes-basic dev
288+
```
289+
290+
## Documentation
291+
292+
- [Getting Started](docs/getting-started.md) / [简体中文](docs/getting-started.zh-CN.md)
293+
- [Aedes adapter](docs/aedes.md) / [简体中文](docs/aedes.zh-CN.md)
294+
- [Events](docs/events.md) / [简体中文](docs/events.zh-CN.md)
295+
- [Service Push](docs/service-push.md) / [简体中文](docs/service-push.zh-CN.md)
296+
- [Kafka bridge](docs/kafka-bridge.md) / [简体中文](docs/kafka-bridge.zh-CN.md)
297+
298+
## Development
299+
300+
```bash
301+
bun run test
302+
bun run typecheck
303+
bun run build
304+
```
305+
306+
## Packages
307+
308+
- `@mqttkit/core`: core application, router, middleware, context, event types, and broker adapter interface.
309+
- `@mqttkit/aedes`: Aedes adapter for TCP MQTT and MQTT-over-WebSocket.

0 commit comments

Comments
 (0)