Documentation

Security

How AppEngine handles passwords, JWTs, API keys, RBAC, and OAuth credentials.

What AppEngine does to protect credentials and tenant data, in plain language. Anything not stated below is not implemented today — we'll only document what's in the codebase.

Password storage

User and customer passwords are hashed with scrypt and a per-record random salt. The implementation lives in src/util/helpers.ts:

// abridged from src/util/helpers.ts
import { randomBytes, scrypt as _scrypt } from 'crypto';
const scrypt = promisify(_scrypt);

export const encryptPassword = async (password: string) => {
  const salt = randomBytes(8).toString('hex');
  const hash = (await scrypt(password, salt, 32)) as Buffer;
  return `${salt}.${hash.toString('hex')}`;
};

The stored value is <salt>.<hashHex>. Verification re-derives the hash with the stored salt and compares. There is no plaintext password anywhere in the database. Reset-token flows store an encrypted temporary password in data.resetToken so the original password is preserved until the user actually completes the reset.

scrypt is a deliberate-cost hash; the same property that makes brute force expensive makes signin slightly slower than a hash like SHA-256. That's the trade-off you want.

JWT signing

Sign-in flows return a JWT signed with the AppEngine signing secret (env-configured). The signing key is HMAC-symmetric, kept on the server, and never exposed to clients. The token carries:

  • The principal's sk (which encodes org + datatype + id).
  • Datatype (user or customer) so the guard can pick the right resolver.
  • Standard claims (iat, exp).

JwtAuthGuard decodes and verifies on every request. Expired tokens return a 401 with an action: refresh_token payload so clients know to call POST /user/refresh (or /customer/refresh) rather than re-prompt for credentials. Malformed tokens return 401 with code: invalid_token. Missing tokens on a non-public route return 401 with code: missing_authorization_header.

Refresh tokens are longer-lived but still time-limited and stored separately from the access token. Rotation on refresh is supported by the strategy layer.

API key handling

API keys are returned once at creation and never readable thereafter — the server stores only the encryptPassword hash of the key (same scrypt scheme as user passwords). The apikey.service.ts does the hashing in encryptApiKey, which calls into encryptPassword.

// from src/users/apikey.service.ts (signature shown for reference)
return await encryptPassword(apiKey);

When a request arrives with x-api-key, the service looks up the key record by its public keyId, re-hashes the supplied secret, and compares. The plaintext key never leaves the original creation response.

Operationally:

  • Keys can be scoped (scopes: ['read', 'write']).
  • Keys can carry rate limits (per minute/hour/day).
  • Keys can have an expiresAt.
  • Rotation: POST /api-key/regenerate/:keyId issues a new plaintext value and invalidates the old.
  • Revocation: DELETE /api-key/:keyId invalidates immediately.
Don't ship keys to the browser

An API key in a Next.js NEXT_PUBLIC_* variable, a mobile bundle, or a public repo is compromised. Treat the same way you'd treat a database password: server-side only, rotated on suspicion.

RBAC: roles + content permissions

Two-layer model.

Roles (@Roles(RoleType.RootAdmin, ...)) gate routes by named role. The principal carries data.roles[]. The guard does an OR-check against the decorator's list. RoleType.RootSystem is special-cased — it always passes.

Content permissions (@RequirePermissions(PermissionTypeContent.create)) gate by per-action permission. The principal carries data.permissions.content[] containing some subset of create, read, update, delete. Both the role and permission check must pass for a request to proceed.

There are also implicit allowances:

  • A user accessing their own profile (/profile/user/... where data.sk === user.sk) is allowed.
  • The author of a record can update or delete their own content even without the role.
  • Records with requiredRole.update / requiredRole.delete arrays grant access if any of those permissions appear on the principal.

The full logic is in src/users/auth/jwt.auth.guard.ts if you want to read the source.

OAuth credential storage

For external integrations (OAuth flows for Google, Facebook, GitHub, Microsoft, Stripe, Twilio, SendGrid, Mailgun, etc.), AppEngine stores tokens via the ConnectModule and UpstreamModule. Stored tokens include the access token, refresh token, expiry, and scopes — keyed by org + vendor.

The credentials live in dedicated collections, not on the user/customer record. Refresh-on-expiry is handled per-vendor by UpstreamModule clients. When a user disconnects a vendor, the credential record is deleted.

We do not document field-level encryption of these credentials at this time — verify against the deployment if your compliance posture requires it.

2FA

Two-factor authentication for both Users and Customers, configured under /profile/security/2fa/*. Supported methods: TOTP and SMS. Setup, verification, enable/disable, and backup-code generation are all exposed.

POST/profile/security/2fa/setup/{method}JWT
POST/profile/security/2fa/verify-setupJWT
POST/profile/security/2fa/disableJWT
POST/profile/security/2fa/backup-codesJWT

When 2FA is enabled, the password sign-in flow requires a follow-up code submission before issuing a JWT.

Device management

The same security module tracks devices a principal has signed in from:

GET/profile/security/devicesJWT
POST/profile/security/devices/{deviceId}/trustJWT
POST/profile/security/devices/{deviceId}/blockJWT
DELETE/profile/security/devices/{deviceId}JWT

Login history is available at GET /profile/security/login-history. Devices can be trusted (skip 2FA prompts on subsequent logins for a configurable duration) or blocked.

What we don't claim

A few things often listed in security pages that we'd rather not assert without evidence in the codebase:

  • Compliance certifications — we don't claim SOC 2, ISO 27001, HIPAA, or PCI here. If your deployment needs them, talk to your operator about what's actually been audited.
  • Field-level encryption at rest — MongoDB encryption-at-rest is a deployment decision, not an AppEngine code feature. Check your cluster config.
  • End-to-end encryption of chat messages — chat is server-relay, not E2E.
  • Hardware-backed key storage — JWT signing keys are env-loaded; HSM integration is not in the open codebase.

If your project needs guarantees beyond what's described here, raise it with your AppMint operator and we'll write a proper security model document for that deployment.

Where to go next