NODERYX
FRAMEWORK v0.7.1

GETTING STARTED

BUILDING

PRODUCTION

DEVICES

REFERENCE

NODERYX / DOCS / PRODUCTION / 03
PRODUCTION / 03

Security

What a new Noderyx application protects by default, and the decisions that remain yours.

01

Secure by default

Security is part of the framework, not a package you remember to install. A newly created application already refuses oversized bodies, rejects forged form posts, sets a strict Content-Security-Policy, and signs its cookies.

Security headers and CSP On. Disable with security: { headers: false }.
CSRF on unsafe methods On for POST, PUT, PATCH, DELETE.
Signed sessions On, stateless, no store required.
Rate limiting 120 requests per minute per address by default.
Request body limit 1 MB, enforced while streaming.
CORS Off—no cross-origin access until you list origins.
Stack traces Development only.
02

The application key

Every signature—sessions, CSRF tokens, signed cookies—derives from APP_KEY. In production the application refuses to start without it; in development a temporary key is generated per process and warned about.

TERMINAL
npx noderyx spark:key # write a key into .env npx noderyx spark:key --show # print one without touching a file
NOTE

Rotating the key signs out every user and invalidates outstanding CSRF tokens, which is why spark:key refuses to overwrite without --force.

03

Content-Security-Policy

Every response carries a policy with a fresh nonce per request. Scripts Noderyx injects carry that nonce; anything injected by an attacker does not. There is no unsafe-inline and no unsafe-eval.

RESPONSE HEADER
default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'nonce-<random>'; style-src 'self'; img-src 'self' data: blob:; connect-src 'self'; upgrade-insecure-requests
NOTE

This means inline event handlers do not run. Use data-noderyx attributes, or pass the request nonce to your own script tag.

04

Widening the policy

When you genuinely need a third-party origin, add it explicitly rather than loosening the whole policy.

noderyx.config.js
security: { headers: { scriptSrc: ["'self'", "https://cdn.example.com"], connectSrc: ["https://api.example.com"], frameAncestors: "'none'" } }
NOTE

Other headers ship on every response: nosniff, Referrer-Policy, X-Frame-Options, COOP, CORP, and HSTS in production.

05

CSRF

Unsafe methods must present a token that is signed, bound to the current session, and matching the cookie. A missing or foreign token returns 419. {{csrfToken}} is available in every rendered view without passing it.

NODERFRAME
form method="post" action="/posts" input type="hidden" name="_csrf" value="{{csrfToken}}" input type="text" name="title" button type="submit" "Publish"
NOTE

From JavaScript, send the X-CSRF-Token header. The native bridge does this for you.

06

Exempting machine clients

Endpoints that authenticate another way—a webhook signature, a bearer token—never rely on a cookie, so CSRF adds nothing but failures.

noderyx.config.js
security: { csrf: { exempt: ["/api/webhooks", /^\/api\/v\d+\/public/] } }
07

Sessions

Sessions are stateless and signed, so nothing needs a store. The cookie is HttpOnly, SameSite=Lax, and Secure in production, and any change re-issues it.

server.js
app.post("/login", async ({ body, session, json, abort }) => { const user = await User.findByEmail(body.email); if (!user || !await verifyPassword(body.password, user.password)) { return abort(401, "Those details did not match"); } session.userId = user.id; return json({ ok: true }); });
NOTE

Because the session lives in the cookie, keep it small—an id, not a profile.

08

Validation

validate() returns only the fields you declared, so unexpected input never reaches the database.

server.js
app.post("/users", ({ validate, json, abort }) => { const { valid, values, errors } = validate({ email: "required|email|max:255", name: "required|string|min:2|max:100", role: "in:admin,editor,viewer" }); if (!valid) return abort(422, Object.values(errors)[0]); return json(User.create(values)); });
NOTE

Rules: required, string, email, integer, number, boolean, url, min:n, max:n, in:a,b,c, alphanumeric, slug.

09

CORS and the packaged app

A packaged Android or iOS build runs from capacitor://localhost or https://localhost, so calls to your API are cross-origin. CORS is off by default—turn it on with an explicit allowlist.

noderyx.config.js
security: { cors: { origins: ["capacitor://localhost", "https://localhost", "https://app.example.com"], credentials: true } }
NOTE

Allowlisted origins are added to connect-src automatically. An unknown origin gets 403 on preflight rather than a listing of what the API accepts.

10

Rate limiting and body size

The limiter answers with 429 and Retry-After, and static files are not counted. Bodies are capped and enforced while streaming, so one request cannot exhaust memory.

noderyx.config.js
security: { trustProxy: true, bodyLimit: 5 * 1024 * 1024, rateLimit: { windowMs: 60000, max: 300 } }
NOTE

Set trustProxy only behind a proxy you control—X-Forwarded-For is trivially spoofed, and trusting it lets one client bypass the limit entirely.

11

Raise the whole posture with one profile

Rather than tightening eight settings by hand, SECURITY_PROFILE selects a coherent set. standard is the framework default; strict and banking progressively harden requests, sessions, and headers.

standard Framework defaults—1 MB bodies, 7-day sessions, SameSite=Lax.
strict 512 KB bodies, 4096-byte URLs, 20s request timeout, content type required, GET bodies rejected, 8-hour sessions with a 30-minute idle timeout, SameSite=Strict, frame-ancestors 'none'.
banking 256 KB bodies, HTTPS enforced, 2048-byte URLs, 15s timeout, 15-minute idle timeout, rolling sessions, two-year HSTS with preload, Referrer-Policy no-referrer, and a Permissions-Policy that denies camera, microphone, geolocation, payment, and USB.
noderyx.config.js
import { securityProfile } from "noderyx-framework"; security: securityProfile("banking", { cors: { origins: ["https://app.example.com"] } })
NOTE

In a generated project the security block writes bodyLimit, rateLimit, and session after the profile spread, so those three come from your solution profile unless you remove the lines. Solution profiles explains the interaction.

12

Request hardening

Beyond the body limit, a request can be refused for its shape before a handler ever sees it. These ceilings are what the strict and banking profiles tighten.

maxUrlLength Longest acceptable request target.
maxQueryParameters Upper bound on parsed query keys.
maxHeaderSize Total header bytes accepted.
requestTimeout / headersTimeout How long a request, and its headers, may take to arrive.
requireContentType Reject bodies that do not declare a content type.
rejectGetBody Refuse a body on a GET, which is almost always smuggling.
enforceHttps Refuse plaintext requests outright rather than redirecting.
NOTE

Every one of these closes a request-smuggling or resource-exhaustion path that a body limit alone does not.

13

Session lifetime, not just session signing

A signed session is not a short one. The stricter profiles add the two timeouts that actually limit an abandoned or stolen cookie.

maxAge How long the cookie is valid at all.
absoluteMaxAge A hard ceiling that no activity can extend.
idleTimeout Expiry after a period with no requests.
rolling Re-issue the cookie on activity, so an active user is not signed out mid-task.
noderyx.config.js
security: { session: { maxAge: 43200, absoluteMaxAge: 43200, idleTimeout: 900, rolling: true, sameSite: "Strict", secure: true } }
NOTE

rolling plus absoluteMaxAge is the combination you usually want: activity extends the session, but never past a fixed limit.

14

API keys for machine clients

A bearer credential is the right authentication for a client that has no cookie jar. The framework generates the token, stores only its hash, and compares in constant time—so a database leak does not hand over working credentials.

server.js
import { createApiKey, requireApiKey } from "noderyx-framework"; const { token, hash, hint } = createApiKey("nyx"); // show `token` once; persist `hash` and `hint` app.use(requireApiKey({ scopes: ["orders:read"], async lookup(hash) { return ApiKey.findByHash(hash); } }));
NOTE

Return an object carrying id, hash, revoked, and scopes from lookup(). A missing or revoked key is refused with 401 and an under-scoped one with 403, both before the handler runs. On success the request carries context.principal, so a handler knows which key it is serving.

15

What else is handled for you

Several classes of bug are closed by the framework rather than by your review checklist.

  • Prototype pollution: __proto__, constructor, and prototype are stripped from parsed bodies.
  • Path traversal: static paths are decoded, checked for null bytes, resolved, and verified to stay inside public/.
  • XSS: {{placeholders}} are HTML-escaped, and data embedded in script blocks escapes < and the line separators.
  • SQL injection: models use parameterised queries and validate table and column names.
  • Error leakage: only messages raised through abort() are shown; malformed JSON returns 400, not 500.
  • Offline cache privacy: no-store, cookie-varying, /api/, and authorized responses are never cached by the service worker.
16

Deployment checklist

Run through this before the first public request.

  • APP_KEY set, unique per environment, and not in version control.
  • NODE_ENV=production, which enables HSTS and disables stack traces.
  • HTTPS terminated in front of the app so Secure cookies work.
  • trustProxy: true only behind a proxy you control.
  • cors.origins lists exactly the origins you expect.
  • Database credentials supplied through environment variables.
  • Rate limits appropriate for real traffic.
  • .env in .gitignore.
TERMINAL
curl -sI https://example.com | grep -i "content-security-policy\|strict-transport\|x-content-type"