JSPM

  • Created
  • Published
  • Downloads 70
  • Score
    100M100P100Q126437F
  • License MIT

Full-stack Node.js framework built on the Mates SPA framework. RPC server functions, WebSockets, file-system routing.

Package Exports

  • mates-fullstack
  • mates-fullstack/browser
  • mates-fullstack/client
  • mates-fullstack/internal
  • mates-fullstack/server

Readme

mates-fullstack

TypeScript-first full-stack framework for Mates apps. It adds direct RPC-style server functions, raw HTTP escape hatches, WebSockets, static assets, and production builds without requiring a compiler in production.

No JSX. No virtual DOM. No Express/Hono/Fastify. Runtime server is pure Node.js (node:http). Bundling and TypeScript transforms use esbuild.


Install

npm install mates-fullstack mates

Add scripts to your app:

{
  "scripts": {
    "dev": "mates-fullstack dev",
    "build": "mates-fullstack build",
    "start": "mates-fullstack start"
  }
}

Project structure

mates-fullstack is convention-based; no config file is required.

my-app/
  client/
    App.ts          # root SPA component and client router
    client.ts       # browser entry point; calls renderX(App, ...)
  server/
    api/            # RPC functions callable from client imports
    helpers/        # server-only utilities; never bundled for browser
    main.ts         # optional middleware registration
    socket/         # optional WebSocket handlers
  shared/           # code/types used by both sides
    public/           # inside client/ — static files copied to dist/public in production
  package.json

Minimal app

client/App.ts

import { html } from "mates";

export default () => {
  return () => html`<h1>Hello from mates-fullstack</h1>`;
};

client/client.ts

import { renderX } from "mates";
import App from "./App";

renderX(App, document.getElementById("app")!);

server/api/todos.ts

import type { ServerCtx } from "mates-fullstack";
import { ValidationError } from "mates-fullstack";

export async function listTodos(_payload: {}, _ctx: ServerCtx) {
  return [{ id: 1, title: "Write docs" }];
}

export async function addTodo(payload: { title: string }, _ctx: ServerCtx) {
  if (!payload.title.trim()) {
    throw new ValidationError("Invalid todo", {
      fields: { title: "Required" },
    });
  }
  return { id: Date.now(), title: payload.title };
}

Client code calls server functions directly

import { asyncAction, html } from "mates";
import { listTodos } from "../server/api/todos";

const TodoPage = () => {
  const load = asyncAction(() => listTodos({}));
  load();

  return () => html`
    <pre>${JSON.stringify(load.data(), null, 2)}</pre>
  `;
};

During development and production builds, imports from server/api/** are converted into browser-safe RPC stubs. Imports from any other server/** path are blocked from client code.


Commands

npm run dev      # native ESM dev server, no browser bundle
npm run build    # production client bundle + compiled server runtime
npm run start    # start production server from dist/

Development mode

Development intentionally serves source modules instead of a client bundle:

  • app source modules: /_src/**
  • package modules: /_pkg/**
  • framework helpers: /_mates/**
  • import map injected into the HTML shell
  • chokidar watches client/ (including client/public/), server/, shared/, .env*, and config files
  • reload generation is owned by the parent dev process and delivered through SSE

This makes stack traces point at source-like module URLs and keeps rebuilds fast.


Authentication

Email/Password + Social Login

mates-fullstack provides a complete auth system out of the box. Access tokens are short-lived (15 min default), stored in httpOnly cookies. Refresh tokens (30 days) live in a separate httpOnly cookie and rotate automatically, with JTI replay detection.

// server/main.ts
import { matesAuth, auth, useArctic } from "mates-fullstack";

matesAuth({ secret: process.env.AUTH_JWT_SECRET! });

useArctic({
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    onSuccess: async (profile, ctx) => {
      const user = await db.users.upsert({ ... });
      await auth.login(ctx, { userId: user.id, email: user.email });
    },
  },
});

Routes /auth/google and /auth/google/callback are registered automatically. onSuccess gives you full control — call auth.login() to issue tokens, or skip it to just link accounts.

10 built-in OAuth providers: Google, GitHub, Discord, Microsoft, Twitter, LinkedIn, Facebook, Apple, Spotify, GitLab. Custom providers via arcticProvider({ handler, ... }).

Cross-Domain SSO

For apps on different domains sharing one auth server. The auth server signs a 30-second code JWT with a shared secret — each app verifies it locally, no round-trip needed.

// auth.com/server/main.ts
import { useSsoProvider } from "mates-fullstack";

useSsoProvider({
  secret: process.env.SSO_SECRET!,
  login: "/login",
  allowedOrigins: ["https://app1.com", "https://app2.com"],
});

// app1.com/server/main.ts
import { useSsoClient } from "mates-fullstack";

useSsoClient({
  authUrl: "https://auth.com",
  secret: process.env.SSO_SECRET!,
  protected: ["/dashboard", "/settings"],
});

Each app issues its own httpOnly session cookies. Works with social login — put useArctic() on the auth server, useSsoClient() on every app.


Middleware

All middleware registers globally in server/main.ts and runs in registration order.

Recommended server/main.ts:

import {
  matesAuth, requestLogger, useSecurityHeaders, useCors, rateLimit,
} from "mates-fullstack";

matesAuth({ secret: process.env.AUTH_JWT_SECRET! });
requestLogger({ skip: [/^\/health$/, /^\/_mates\//] });
useSecurityHeaders();
useCors({ origins: "https://app.example.com" });
rateLimit({ max: 10, window: "15m", pathPrefix: "/auth" });
rateLimit({ max: 300, window: "1m" });

Request Logger

requestLogger({ skip: [/^\/health$/, /^\/assets\//] });

Logs every request (method, path, IP) on arrival and every RPC response (status, ms) on completion. Stamps x-request-id on every response. Structured logging — NDJSON in production, pretty-printed in dev.

Security Headers

useSecurityHeaders();
useSecurityHeaders({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: "DENY" });

Sets 14 standard security headers on every response — complete helmet-equivalent coverage. Every header individually overridable or disableable. HSTS auto-enabled in production, skipped in dev. CSP and Permissions-Policy are opt-in.

CORS

useCors({ origins: "https://app.example.com", credentials: true });
useCors({ origins: (origin) => origin.endsWith(".example.com") });

Handles preflight OPTIONS automatically (returns 204). Reflects origin when credentials: true as required by the CORS spec. Sets Vary: Origin.

Rate Limiting

rateLimit({ max: 100, window: "1m" });                             // all routes
rateLimit({ max: 10, window: "15m", pathPrefix: "/auth" });        // auth only
rateLimit({ max: 5, window: "1h", key: (_req, ctx) => String(ctx.auth.userId) }); // per-user

Sliding-window counter, in-memory. Returns 429 with Retry-After and X-RateLimit-* headers. Auto-evicts expired entries. Custom key function for per-user or per-endpoint limiting.

Raw HTTP hooks

For cases where an external service dictates the HTTP contract (webhooks, custom OAuth flows):

import { onHttpRequest } from "mates-fullstack";

onHttpRequest(async (req, ctx) => {
  const url = new URL(req.url);
  if (req.method === "POST" && url.pathname === "/stripe-webhook") {
    const body = await req.text();
    await handleWebhook(body, ctx.reqHeaders["stripe-signature"]);
    return new Response("ok");
  }
});

Request Limits / Resource Protection

Built-in limits prevent CPU and memory exhaustion. All have safe defaults — override only to tune for your app.

// server/main.ts — at the top, before middleware
import { configureRpcRunner } from "mates-fullstack/internal";

configureRpcRunner({
  maxJsonBytes: 5 * 1024 * 1024,     // default 1MB
  maxJsonDepth: 30,                    // default 50
  maxArrayLength: 5000,                // default 10_000
  maxStringLength: 50_000,             // default 100_000
  maxUploadBytes: 50 * 1024 * 1024,   // default 10MB
  maxMultipartParts: 100,              // default 200
});

Or set server-level limits when starting:

await startServer({
  maxConnections: 2000,           // default 1000
  staticStreamTimeout: 120_000,   // default 60s
  maxSseClients: 1000,            // default 500
});

All defaults are active without any configuration call. See DOCS.md for full details.


Public entrypoints

Import Use for
mates-fullstack stable app API: errors, redirects, middleware registration, ServerCtx, validation/sanitize/upload helpers
mates-fullstack/client browser hydration and RPC helpers
mates-fullstack/browser browser-safe error/redirect/stream/download helpers
mates-fullstack/server server-safe helpers and middleware runners
mates-fullstack/internal unstable build/runtime/scanner internals for tooling

License

MIT