Ory Polis: the SAML bridge that still calls itself Jackson
Streamline your web application's authentication with Polis, an SSO service supporting SAML and OpenID Connect protocols. Beyond enterprise-grade Single Sign-On, it also supports Directory Sync via the SCIM 2.0 protocol for automatic user and group provisioning/de-provisioning.
At a glance
- What is it?
- Ory Polis, the project formerly called BoxyHQ Jackson, turns an upstream SAML or OpenID Connect login into an ordinary OAuth 2.0 flow and syncs accounts over SCIM 2.0. The code runs cleanly on port 5225, but the shipped secrets and the half finished rename are worth reading before anything reaches production.
- Who is it for?
- Adopt Polis when a SAML or OIDC provider already exists upstream and your application only speaks OAuth 2.0, then run it against a database engine your team can back up. Leave it alone for plain password or social login, where it adds a hop without translating anything.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 88 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The SAML handshake your application never sees
Ory Polis, the project formerly called BoxyHQ Jackson, sits between enterprise identity providers and the applications that need to log users in. It takes a login that starts as SAML or OpenID Connect at an upstream provider and hands the same login to your application as an ordinary OAuth 2.0 flow. Your application never parses an assertion, never fetches a metadata URL, and never signs a request with its own SAML certificate.
The repository tags name both protocol roles, listing saml-identity-provider and saml-service-provider alongside oidc and openid-connect, and the README states that Polis implements single sign-on as an OAuth 2.0 flow while abstracting the complexities of the underlying SAML or OIDC protocol. Two upstream families are handled by one service: SAML providers such as corporate identity platforms, and OpenID Connect providers.
Per login the data moves in a fixed direction. A user opens your application, your application redirects to Polis, Polis forwards an authentication request upstream, the provider answers with an assertion or an ID token, and Polis converts that into the tokens your application consumes. Directory provisioning is a separate channel that never runs inside the login path.
SCIM 2.0 sync wired to two Google variables
Polis also speaks SCIM 2.0 for directory sync, covering provisioning and de-provisioning of users and groups rather than login itself. The README presents this as a way to hold one source of truth for identity data and cut the manual hours spent on user lifecycle work.
The first detail worth pausing over is where directory sync is configured. Its credentials live in the compose file and nowhere else in the shipped configuration:
- DSYNC_GOOGLE_CLIENT_ID=${DSYNC_GOOGLE_CLIENT_ID}
- DSYNC_GOOGLE_CLIENT_SECRET=${DSYNC_GOOGLE_CLIENT_SECRET}Neither key appears in .env.example. A developer who copies that file, points DB_URL at a database and starts the service ends up with no directory sync configuration and no error message about it. The upstream in the shipped configuration is Google Workspace, and no other directory provider shows up among the environment keys of the compose file or .env.example.
Running Polis locally on port 5225 with compose
The package scripts cover the local loop, and the databases come from a compose file that is not the one in the repository root:
"dev": "cross-env API_KEYS=secret IDP_ENABLED=true next dev --webpack -p 5225",
"dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d",
"postgres": "cross-env API_KEYS=secret DB_ENGINE=sql DB_TYPE=postgres DB_URL=postgres://postgres:postgres@localhost:5432/postgres npm run dev"Start the databases first, then run the app script, which serves on 5225. Note that dev-dbs targets ./_dev/docker-compose.yml, so the file at the root is a different artifact: it pulls a published image and assembles the environment for you.
jackson:
image: boxyhq/jackson:26.2.0
environment:
- API_KEYS=secret
- DB_URL=postgres://postgres:postgres@postgres:5432/postgres
- EXTERNAL_URL=http://localhost:5225
- NEXTAUTH_URL=http://localhost:5225
- NEXTAUTH_SECRET=secret
- POLIS_LICENSE_KEY=${POLIS_LICENSE_KEY}The image tag matches the version declared in package.json, the port mapping publishes 5225 to 5225, and the database host is postgres on the internal jackson network. Nothing there builds the app from source. If you want that, the Dockerfile pins its base image and switches off Next.js telemetry while compiling:
ARG NODEJS_IMAGE=node:24.18.0-alpine3.24
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
RUN apk update && apk upgradeAfterwards you should reach the admin portal on 5225 with no admin credentials set, which means the portal stays shut until you supply NEXTAUTH_ADMIN_CREDENTIALS or an access list.
Six database engines behind one set of variables
Polis does not choose a database for you. DB_ENGINE and DB_URL are the contract, and the package scripts enumerate the combinations that work in development:
"mongo": "cross-env API_KEYS=secret DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson npm run dev",
"pre-loaded": "cross-env API_KEYS=secret DB_ENGINE=mem PRE_LOADED_CONNECTION='./_dev/saml_config' npm run dev",
"dynamodb": "cross-env API_KEYS=secret DB_ENGINE=dynamodb DB_URL='http://localhost:8000' npm run dev"The engine values are sql, mongo, mem, planetscale and dynamodb, with DB_TYPE narrowing the sql branch to postgres, mysql, mariadb or mssql. Each carries its own default port in those scripts: 5432 for postgres, 3307 for mysql, 3306 for mariadb, 1433 for mssql, 27017 for mongo and 8000 for a local DynamoDB. The mem engine has no server at all and reads a connection config from disk through PRE_LOADED_CONNECTION, which makes it the quickest way to try a SAML flow with nothing installed.
Tuning keys sit beside the connection string: DB_TTL=300 for retention, DB_CLEANUP_LIMIT=1000, DB_PAGE_LIMIT=50, and a DB_ENCRYPTION_KEY you generate as a random 32 byte value. The root of the repository holds 14 skaffold files, one per database and several demo mocksaml combinations, so a local stack and a CI run can be stood up per engine.
The shipped admin secrets are the word secret
Two configuration lines ship in a form you must not keep:
API_KEYS="secret"
NEXTAUTH_SECRET=secret
DB_ENCRYPTION_KEY=The comment telling you to change the value sits next to NEXTAUTH_SECRET only, with an openssl hint for a random 32 byte key. API_KEYS carries no such warning, yet every development script in package.json and the root compose file set it to that same string, so the value an attacker reads first is published in this repository. The database section has the same shape: DB_ENCRYPTION_KEY is empty and nothing above the blank line says what an empty key does to stored connection secrets.
The trust settings in the compose file make the same point twice. The database is started with an empty password and host auth set to trust, yet the application receives a URL with a postgres and postgres credential pair:
postgres:
image: postgres:18.4
environment:
- POSTGRES_PASSWORD=''
- POSTGRES_HOST_AUTH_METHOD='trust'
ports:
- 5432:5432Trust authentication ignores the password, so the stack works, and the example teaches a credential shape the database will not enforce once you tighten POSTGRES_HOST_AUTH_METHOD.
BoxyHQ strings that survived the rename
Polis was renamed from BoxyHQ Jackson, and the rename stopped at the prose. package.json still declares its description as a SAML 2.0 service with the single keyword SAML 2.0, although the project presents itself as SAML plus OIDC plus SCIM. The compose file names both its service and its network jackson, pulls boxyhq/jackson:26.2.0, and the mongo script points at a database literally named jackson on 27017. The default SAML_AUDIENCE in .env.example is https://saml.boxyhq.com, the admin portal tenant default is the string _jackson_boxyhq, and a demo recording at the repository root is still called samljackson480.gif.
Operationally this is harmless in a scratch deployment and confusing in every other one. A search for jackson across your own tooling now returns Polis, the SAML offering on Ory's hosted network, and an image published under a different vendor namespace that the documentation has moved away from. If you pin that image by name in a helm chart or a kustomize overlay, you are pinning to that namespace.
The version numbers tell a second story. package.json declares version 26.2.0 under Apache-2.0, the last push was on 2026-07-27, and the project history still lists v1.52.1 and v1.52.0 from 2025-07. Two numbering lineages coexist, the 1.x line and the current one, so a version string alone does not tell you which series a deployment came from.
NEXTAUTH_ACL denies everything when it is empty
The admin portal has two doors and one of them fails closed. Magic link access is controlled by NEXTAUTH_ACL, a comma separated string of email addresses or glob patterns such as `[email protected],*@marvel.com`, and the configuration file states that addresses which do not match are denied and that specifying no value denies access to all. With NEXTAUTH_ACL empty in .env.example, a fresh deployment has no magic link access at all until somebody sets it.
The second door is NEXTAUTH_ADMIN_CREDENTIALS, which takes email:password pairs and allows several logins when comma separated. SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD and SMTP_FROM back the magic link mail itself, so an access list without working SMTP is a closed door either way. Two further keys govern portal sign in for an SSO tenant, ADMIN_PORTAL_SSO_TENANT and ADMIN_PORTAL_SSO_PRODUCT, and both defaults still carry jackson inside their values.
Instrumentation is opt in through OTEL_EXPORTER_OTLP_METRICS_ENDPOINT and OTEL_EXPORTER_OTLP_METRICS_HEADERS, with a commented OTEL_EXPORTER_OTLP_METRICS_PROTOCOL set to grpc for teams that prefer that transport. Neither endpoint nor header ships with a value, so nothing is exported until an operator supplies one.
Where Polis is the wrong shape for the job
Polis needs something upstream to talk to. If your users already authenticate through an OpenID Connect provider and your application already consumes OAuth 2.0 tokens, the extra hop buys translation you do not need. Ory's own ecosystem list names Ory Hydra as an OAuth2 and OpenID Connect server and Ory Kratos as identity and user infrastructure and management, and a stack built from those two replaces the translation layer instead of adding one.
Polis is also not where user records live. Directory sync pulls accounts in from an external directory, and the user management story points at Ory Kratos, so a team that wants to own passwords and profile data brings another service. The same goes for an application with no enterprise directory: a small SaaS on password and social login gains nothing from an SSO bridge. The Dockerfile shows the deployment shape, a Next.js application on node:24.18.0-alpine3.24 with python3, py3-pip, make and g++ installed during the dependency build for native modules.
One boundary is worth naming before procurement. The README places advanced scaling and multi tenancy features, security patching on a severity schedule with strict SLAs, and access to a private Docker registry behind the paid Ory Enterprise License, which in turn needs POLIS_LICENSE_KEY. Self hosters who reach a ceiling in the open source build find that the documented remedy is a commercial contract rather than a configuration change.
Editorial conclusion
Adopt Polis when a SAML or OIDC provider already exists upstream and your application only speaks OAuth 2.0, then run it against a database engine your team can back up. Leave it alone for plain password or social login, where it adds a hop without translating anything. Before deploying, change API_KEYS and NEXTAUTH_SECRET away from the shipped value, point EXTERNAL_URL and NEXTAUTH_URL at the same public address, set NEXTAUTH_ACL or you get no admin portal at all, and decide whether DSYNC_GOOGLE_CLIENT_ID belongs in your compose file.
Frequently asked questions
What does Ory Polis replace in an authentication flow?
It replaces the SAML handshake your application would otherwise handle. Polis proxies a SAML or OpenID Connect login from an upstream identity provider and presents it to your application as an OAuth 2.0 flow, and it also handles SCIM 2.0 directory sync for provisioning and de provisioning of users and groups.
Which databases can Ory Polis run on?
The package scripts show the sql engine with DB_TYPE set to postgres, mysql, mariadb or mssql, plus mongo, planetscale, dynamodb and an in memory mem engine that reads PRE_LOADED_CONNECTION from disk. DB_ENGINE and DB_URL carry the choice, and each script carries its own default port.
How do I enable directory sync in Ory Polis?
The root compose file expects DSYNC_GOOGLE_CLIENT_ID and DSYNC_GOOGLE_CLIENT_SECRET, and those two keys are the only directory sync credentials in the shipped configuration. Neither appears in .env.example, so you have to add them yourself.
Why can I not log in to the Ory Polis admin portal after starting it?
NEXTAUTH_ACL is empty in .env.example, and the file states that an empty value denies access to all, so the magic link door is closed. Set NEXTAUTH_ACL to your addresses or glob patterns, or use NEXTAUTH_ADMIN_CREDENTIALS with email:password pairs.
Does Ory Polis still use the Jackson name in its configuration?
It does. The compose service and its network are named jackson, the image is boxyhq/jackson:26.2.0, package.json describes the project as a SAML 2.0 service, and the default SAML_AUDIENCE points at saml.boxyhq.com even though the project is now called Ory Polis.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ory-polis)Was this useful?