Short term (0-3 months)

Hardening and gap-fills - what gets attention next.

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.

PackageStatusWhy experimentalWhat promotes it
@miiajs/cliexperimental49 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-redisexperimental14 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.

PackageTests todayEdges worth adding when we touch them
@miiajs/jwt6Algorithm allowlist enforcement, expired tokens, key rotation, missing config.
@miiajs/auth12Multi-strategy resolution, token extractor edge cases, AuthGuard composition.

Multipart / file upload (@miiajs/multipart)

planned

await 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 as ReadableStream, so the parser is the runtime's job.
  • Node.js / uWebSockets - busboy (the battle-tested stream parser that powers multer and @fastify/multipart under 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)

planned

Own 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: RateLimitStore interface, @RateLimit({ window, max, key? }) decorator, middleware form, standard RateLimit-* response headers, in-memory store (default for dev).
  • @miiajs/rate-limit-redis - ioredis-backed store using a Lua script for atomic INCR + 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

planned

Per-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

planned

ctx.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 LoggerService interface 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/health package - 50 lines of glue do not earn one.
  • Transactions recipes: explicit db.transaction() patterns for Drizzle, Mongoose, Papr. No @Transactional decorator, no AsyncLocalStorage indirection.

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).