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.

Recently shipped

  • Rate limiting - @miiajs/rate-limit shipped in 0.2 (beta): fixed-window limiter with a RateLimitStore contract whose increment() counts the hit and decides blocking atomically; the guard flow with @RateLimit / @SkipRateLimit (replacement semantics, @BodyLimit-style precedence) plus a perimeter rateLimit() middleware; draft-6 RateLimit-* headers; ctx.ip / trustProxy landed in core to support it. Deliberate departures from the original plan below: one package instead of three, fixed window instead of token bucket, increment() instead of consume() - a simpler default, with the store contract already shaped for Redis.
  • Request body size limits - shipped in 0.2.0: maxBodySize app option + @BodyLimit per-route overrides, enforced in core and in the adapters. See Body size limits.
  • Multipart / file upload - @miiajs/multipart shipped in 0.6 (beta): @Multipart opens a multipart/form-data route, ctx.parts walks the body part by part with real backpressure, ctx.form() buffers the same body into Files and strings, and @ValidateForm checks the flat form against a schema. The original plan on this page said one parser per runtime - busboy on Node, the runtime's own formData() on Bun and Deno. Both halves were wrong: native formData() buffers every part in memory on every runtime, so there was no streaming path to delegate to, and a Node-shaped stream parser would have left the edge runtimes without one. What shipped is a single engine everywhere - multipasta behind our own Web Streams bridge - so per-part streaming, per-file limits, and error mapping read the same on Bun, Deno, Node, and uWebSockets (backpressure reaches the socket on all four). allowedTypes filters file parts by media type at the head of a part, before its body is read - the media type is still client-supplied, so it buys an early refusal rather than a guarantee about the bytes. The worked example is the uploads module in examples/full-app, next to the raw-formData() avatar upload it contrasts with.

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/cliexperimental51 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-redisexperimental30 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.

Rate limiting stores (@miiajs/rate-limit-redis + @miiajs/rate-limit-upstash)

planned

The core package shipped in 0.2 with an in-memory store. What remains is the distributed story - two store packages built against the existing RateLimitStore interface:

  • @miiajs/rate-limit-redis - ioredis-backed store using a single Lua script for the atomic increment + window expiry + block decision. 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.

Sliding window and token bucket algorithms are candidate extensions of the RateLimitStore contract, most likely landing together with the Redis store (fixed window shipped first as the simpler default).

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). The RateLimitStore interface is ~20 lines 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.

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:

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