Skip to content

Integration

Foxnox does not sit on the public internet by itself. Getting it working means wiring up three connections: a Backend for Frontend in front of it, your user management service beside it, and the public URLs users will see.

The BFF authenticates callers, issues sessions, and proxies /foxnox and /foxnox/web. Gatelin is the reference implementation used below (PWD_CHECK_URL, route seed, /api prefix). Another BFF that speaks the same HTTP contract is fine.

1. Point the BFF at Foxnox

With Gatelin, each internal endpoint gets its own variable:

PWD_CHECK_URL=http://my-project-foxnox-local:3000/foxnox/compare
PWD_CHALLENGES_URL=http://my-project-foxnox-local:3000/foxnox/challenges
PWD_TRUSTED_DEVICES_URL=http://my-project-foxnox-local:3000/foxnox/devices/verify
PWD_LOGIN_TICKET_URL=http://my-project-foxnox-local:3000/foxnox/login-tickets/redeem

Only the first is mandatory; the other three are what the mid-login challenges need. A custom BFF should call the same endpoints:

EndpointUsed for
POST /foxnox/challengesMinting a 2FA / expired-password / trusted-device challenge
POST /foxnox/devices/verifyChecking the trusted_device cookie to skip 2FA
POST /foxnox/login-tickets/redeemRedeeming the one-shot ticket that finishes a session

Use the container hostname on the internal Docker network, not the public URL. Foxnox has no public API route, and routing password checks back out through Traefik would expose them.

2. Point Foxnox at User Management

Foxnox knows users only by numeric ID. When a user types their email address into the "forgot password" form, Foxnox has to translate it:

USER_SEARCH_URL=https://users.internal.example/users/search

This service is an external production dependency, not part of the Foxnox production image or Compose stack. Set the same endpoint on the BFF for login. Production Compose fails before startup when USER_SEARCH_URL is unset. Without a working endpoint, email-driven workflows cannot resolve an address and show their non-enumerating confirmation page without sending anything.

3. Set the Public URLs

Because requests arrive through the BFF, Foxnox cannot work out its own public address. Tell it explicitly:

WEB_PUBLIC_ORIGIN=https://app.example.com
WEB_PUBLIC_BASE=/api/foxnox/web
WEB_LOGIN_RESUME_URL=https://app.example.com/foxnox/login

WEB_PUBLIC_BASE is the public routing prefix (/api) plus Foxnox's own mount (/foxnox/web). If you change Traefik's strip-prefix rule, change this too — otherwise every reset link in every email will 404.

4. Register Foxnox with the BFF (Gatelin example)

Gatelin only forwards requests that match a registered route, so Foxnox's endpoints have to exist as rows in Gatelin's database. A Foxnox checkout ships this seed data at db/liquibase/gatelin-data/; mount that exact folder into Gatelin's migration container. A different BFF needs its own equivalent: public workflow paths, protected JSON CRUD, and internal-only compare/challenge/verify/redeem calls.

yaml
gatelin_migration:
  image: ghcr.io/alten-group/gatelin-migration:latest
  volumes:
    - ./db/liquibase/gatelin-data/:/liquibase/data

The seed registers:

ChangesetWhat it adds
01-service.sqlThe foxnox service, with an empty pattern because the password router is mounted at the Express root
02-resource.sqlResources foxnox, foxnox/tokens, foxnox/policies, foxnox/devices (Gatelin caps resource.name at 20 characters)
03-route.sqlThe 25 JSON CRUD routes (including /foxnox/compare), all protected
04-permission.sqlGrants those routes to the Super admin (role 1) and Admin (role 2) roles
0508The foxnox/web resource and every account workflow page route
09-route-challenges.sqlThe foxnox/challenges resource and the challenge-minting route
10-cors.sqlAllowed origins
1113Admin table-preference resources (passwords, policies, tokens, trustedDevices), default column layouts, and Gatelin preference scopes

POST /foxnox/devices/verify and POST /foxnox/login-tickets/redeem are not proxied CRUD routes. The BFF calls them on the internal network (Gatelin uses PWD_TRUSTED_DEVICES_URL and PWD_LOGIN_TICKET_URL).

Protected vs. public routes

This distinction is the heart of the integration, and it is easy to get wrong. A route registered as protected requires a valid session; a public one does not.

Route groupProtectedWhy
All JSON CRUD (/foxnox/…)Administrative data; only admins should read or write it
/foxnox/challengesOnly the BFF mints challenges
/foxnox/web/recover, /foxnox/web/unlockThe user has forgotten their password — by definition they cannot be signed in
/foxnox/web/2fa/verify, /foxnox/web/password/expired, /foxnox/web/trusted-devices/promptMid-login: the password was accepted but no session exists yet. Access is gated by the challenge token in the URL, not by a session.
/foxnox/web/account-recoverThe user cannot produce a 2FA code, so they cannot sign in
/foxnox/web/2fa/setup, /foxnox/web/security-questions, /foxnox/web/trusted-devicesManaging your own security settings requires proving who you are first

Field and condition ACL

For protected JSON CRUD routes, the BFF forwards its resolved data restrictions:

HeaderFoxnox enforcement
x-acl-fieldsFilters write rows and projects read rows, history snapshots, and schema fields. Omitted is unrestricted; empty means only id.
x-acl-conditionsForces search predicates with AND, constrains inserts, and checks update/archive/history IDs before access.

Foxnox accepts condition operators =, !=, <, >, <=, and >=. It rejects malformed JSON, unknown or non-filterable fields, object/array values, unsupported operators, and more than 50 conditions with 403.

Keep Foxnox and the BFF on the same trusted internal network and give Foxnox no public Traefik API router, host port, or external load-balancer target. Foxnox trusts the BFF to authenticate callers and construct ACL headers. An omitted ACL header intentionally means unrestricted for protected administrator permissions and trusted internal flows, so exposing Foxnox directly would let a client bypass the BFF by omitting those headers.

5. Seed the First Passwords

A fresh Foxnox database has a schema, seeded token types, and a default password policy — but no passwords. Create one per user by posting the user IDs; the service generates and hashes the plaintext itself:

POST /api/foxnox/
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "rows": [
    { "userId": 1 },
    { "userId": 2 }
  ]
}

The response contains the generated plaintext once. Save it or send it to the user immediately; it is not recoverable afterwards. See Passwords.

Foxnox pages are only useful if users can reach them. Two links matter:

WhereLink to
Login page, "Forgotten password?"/api/foxnox/web/recover
Account settings, "Two-factor authentication"/api/foxnox/web/2fa/setup
Account settings, "Remembered devices"/api/foxnox/web/trusted-devices

For the admin UI shipped with Gatelin, setting ADMIN_PASSWORD_RECOVERY_URL=/api/foxnox/web/recover adds the first link automatically.

Your login code also has to handle the 202 challenge response, or users with 2FA enabled will never be able to sign in. See Frontend Integration.

Verifying the Integration

Work through these in order — each one depends on the last:

bash
# 1. Foxnox is up and can reach its database
docker exec my-project-foxnox-local wget -qO- http://localhost:3000/foxnox/health/ready

# 2. The BFF can reach Foxnox (Gatelin example)
docker exec my-project-gatelin-local nc -zv my-project-foxnox-local 3000

# 3. A workflow page renders through the BFF
curl -i http://localhost:8100/api/foxnox/web/recover

# 4. A login succeeds end to end (Gatelin session endpoint)
curl -i -X POST http://localhost:8100/api/gatelin/sessions \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","pwd":"<plaintext>"}'

Step 3 returning 404 almost always means the foxnox/web routes were not registered on the BFF (for Gatelin, the seed in db/liquibase/gatelin-data/). Step 4 returning 202 is not a failure — it means a login challenge is required, which is the subject of Login Challenges.

Published and maintained by ALTEN