Short term (0-3 months)
The short-term horizon is mostly debt and gaps in existing packages rather than new things. The goal is to take everything currently labelled experimental to beta and unblock common app shapes that today require user-supplied glue.
Validating the experimental packages
Two packages currently sit at experimental because they have not been validated by real adoption, not because their internal coverage is poor.
| Package | Status | Why experimental | What promotes it |
|---|---|---|---|
@miiajs/cli | experimental | 49 tests, but the 11 scaffold features have not been exercised by real miia new runs in production projects. | Run miia new to bootstrap a real app, fix anything that breaks, write end-to-end scaffold tests. |
@miiajs/messaging-redis | experimental | 14 tests; 1 still TODO (survives consumer crash via XAUTOCLAIM, marked it.todo). No production deployment. | Build a small worker app on top of it, exercise crash recovery, write the XAUTOCLAIM test. |
@miiajs/auth/oauth2 recipe page is also marked experimental until the GitHub OAuth flow is validated end-to-end on a real app. The plan is to keep OAuth2 as a recipe rather than a separate package - see Examples and docs gaps.
Test depth on small-surface packages
These are beta (small but well-targeted test coverage) but would benefit from a few more edge-case tests as we hit them in real apps. None block their beta status today.
| Package | Tests today | Edges worth adding when we touch them |
|---|---|---|
@miiajs/jwt | 6 | Algorithm allowlist enforcement, expired tokens, key rotation, missing config. |
@miiajs/auth | 12 | Multi-strategy resolution, token extractor edge cases, AuthGuard composition. |
Multipart / file upload (@miiajs/multipart)
plannedawait ctx.req.formData() already works today via Web Standards - good enough for small uploads where loading the whole body into memory is fine. The gap is streaming large files and a uniform UploadedFile shape across runtimes.
The package will be a thin per-runtime layer behind a single decorator. Each runtime gets the fastest streaming path it natively supports:
- Bun / Deno - native streaming
formData(). Bun 1.1+ and Deno 1.40+ both expose multipart parts asReadableStream, so the parser is the runtime's job. - Node.js / uWebSockets -
busboy(the battle-tested stream parser that powersmulterand@fastify/multipartunder the hood).
Method-level decorator (similar to @ValidateBody) wraps a Koa-style middleware - no new framework concept, just a more ergonomic surface than raw @Use. Built-in: file size limits, MIME filters, abort handling, direct pipe to your storage of choice (S3 SDK, GCS client, local filesystem). We do not reimplement the multipart protocol on Node.js; busboy does the parsing. Bun/Deno parsing stays delegated to the runtime. The exact API surface gets locked in once we exercise it on a real upload-heavy app.
Why now: every real app eventually needs file upload, and "use formData() for small files, write your own busboy plumbing for large ones" is not a great pitch for a "decorators + Web Standards" framework.
Rate limiting (@miiajs/rate-limit + @miiajs/rate-limit-redis + @miiajs/rate-limit-upstash)
plannedOwn implementation, not a wrapper. Token bucket and sliding window built against a small RateLimitStore interface with atomic consume(key, points, window) semantics. Three packages ship together:
@miiajs/rate-limit- core:RateLimitStoreinterface,@RateLimit({ window, max, key? })decorator, middleware form, standardRateLimit-*response headers, in-memory store (default for dev).@miiajs/rate-limit-redis- ioredis-backed store using a Lua script for atomicINCR+ window expiry. Covers typical Node/Bun/Deno deployments.@miiajs/rate-limit-upstash- thin adapter over@upstash/redis(HTTP REST API). This is the edge story: works on Vercel Edge, Netlify, Cloudflare Workers, and any other runtime where TCP-based clients like ioredis don't start.
@Get('/login')
@RateLimit({ window: '1m', max: 5, key: (ctx) => ctx.req.headers.get('x-forwarded-for') })
async login(ctx) { ... }
Why our own. Wrappers over rate-limiter-flexible looked appealing but bring two problems: official Bun support is unverified, and the library's store list is Node-shaped (no native edge story). A token bucket is ~150 lines, the RateLimitStore interface is ~20, and we control runtime compatibility by construction. Same pattern as IdempotencyStore in messaging - users learn one shape across the framework.
Serverless story. Unlike cache, rate limiting needs atomic counter increments with TTL. Unstorage's getItem/setItem is not atomic - we cannot reuse that bridge here without race conditions under load. Instead, the dedicated @miiajs/rate-limit-upstash package gives Vercel/Netlify/CF Workers users a working store from day one.
Because RateLimitStore is our interface, native first-party drivers (@miiajs/rate-limit-cloudflare-do for Durable Objects, @miiajs/rate-limit-dynamodb for AWS Lambda) can land later without breaking users. They swap the store instance, nothing else changes.
Why now: rate limiting is table-stakes for any public API. Owning the implementation lets us ship a clean serverless story without depending on a third-party library's stance on edge runtimes.
Request body size limits
plannedPer-app and per-route ceiling on incoming body size. Configured via middleware factory (bodyLimit('1mb')) and a decorator (@BodyLimit('500kb') for overrides). Adapters that buffer (node-server / uws-server optimized mode) enforce the cap before allocating; streaming paths abort the stream when the cap is reached. Returns 413 Payload Too Large with a structured exception.
Why now: missing body limits is a common DoS surface. Adapters today have an internal bufferThreshold for the buffered fast path, but no enforced ceiling for streaming - the user has to write it, and most don't.
Server-Sent Events helper
plannedctx.res.sse(stream | iterable) helper plus a small SseStream builder that handles event framing, keep-alive comments, and back-pressure. No decorator, no new package - this lives in @miiajs/core and composes with existing guards/middleware/DI. There will not be a @miiajs/sse package.
@Get('/events')
async stream(ctx: RequestContext) {
return ctx.res.sse(async function* () {
yield { event: 'tick', data: { now: Date.now() } }
})
}
Why now: SSE solves 70% of "I need server-push" use cases (notifications, progress updates, log tails) without WebSocket-level complexity. WebSocket lands mid-term; SSE is ~50 lines of code today and unblocks real apps now.
Request validation pipe sugar
idea@ValidateBody/@ValidateQuery/@ValidateParams exist but are Zod-flavoured. Add a thin pipe layer so consumers can plug arbitrary schema libraries (Valibot, ArkType, Yup) by passing a safeParse-shaped function. Mostly already there via ZodLike interface; needs docs and one or two non-Zod tests to demonstrate.
Examples and docs gaps
Add small, focused example apps and recipes so contributors and users can copy-paste:
examples/multipart-upload- file upload with progress, alongside the new package.- Auth recipes: validate the existing OAuth2 recipe end-to-end with GitHub/Google, add API-key recipe, add session-based recipe. Recipes only - no separate auth packages (per design choice).
- Production logger recipes: how to plug winston / pino into the existing
LoggerServiceinterface for structured JSON logs. The interface is already there; the docs aren't. - Health check recipe: small core helper plus a controller pattern. We deliberately do not ship a
@miiajs/healthpackage - 50 lines of glue do not earn one. - Transactions recipes: explicit
db.transaction()patterns for Drizzle, Mongoose, Papr. No@Transactionaldecorator, noAsyncLocalStorageindirection.
Out of scope (deferred to mid-term)
- WebSocket support
- OpenTelemetry integration
- Cache abstraction
These are valuable but bigger commitments than 0-3 months. Items removed from short-term entirely after re-scoping: dedicated health-checks package (now a recipe), separate auth-strategy packages (recipes only, never packages), Request-Reply for messaging (long-term), scheduler (long-term idea).