MainSDK
Search⌘K
How-to

Make your app enterprise ready

Give your own customers SAML or OIDC SSO, SCIM, phishing-resistant admin sign-in, an audit log, retention rules and legal hold, free in your app.

Updated 2026-10-28

Answer an enterprise buyer's security questionnaire with features your app already has. Everything here runs in the free runtime, inside your app: it is what your app does for your customers, not a paid plan. The example customer is Pinecart's enterprise buyer, with the domain pinecart.com.

Before you start: a MainSDK app with sign-in on auth and data on db. See Sign-in and sessions.

1. Turn on SSO per customer organisation

SAML 2.0 and OIDC single sign-on ship in phase 1 (milestone 0.3), configured per customer organisation. Where your identity provider driver lacks SAML or SCIM, MainSDK provides it in your app on top of the driver.

main.yaml
auth:
  driver: github
  methods: [passkeys, sso]
  sso: { protocols: [saml, oidc], scim: true }
  admin: { roles: [admin, support], require: phishing-resistant }
app/admin/sso/actions.ts
"use server";
import { auth } from "mainsdk";

export async function enablePinecartSso(clientSecret: string) {
  await auth.sso.enable("pinecart", {
    protocol: "oidc",
    issuer: "https://pinecart.okta.com",
    clientId: "pinecart-app",
    clientSecret,
    domains: ["pinecart.com"],
  });
}

For a customer on SAML, pass protocol: "saml" with the IdP's metadata and the same domains; see SAML in the auth interface for the two routes it needs. The IdP's certificate comes only from that metadata.

jane@pinecart.com is routed to Pinecart's identity provider by her verified domain. Unsigned assertions, the wrong audience, expired assertions and replayed assertion IDs are refused with MAIN-AUTH-0008.

2. Provision users with SCIM

app/admin/sso/actions.ts
import { auth } from "mainsdk";

const { endpoint, token } = await auth.scim.enable("pinecart", {
  groupRoles: { "Pinecart Admins": "admin", "Pinecart Staff": "editor" },
});

Give the endpoint and token to Pinecart's IT team. Removing someone in their directory ends that person's sessions in your app.

3. Require phishing-resistant admin sign-in

With auth.admin.roles set as in step 1, mainsdk check fails when one of those roles can sign in without passkeys or SSO with phishing-resistant MFA (rule auth.admin_phishing_resistant). That covers PCI DSS 4.0.1 req. 8.4.1.

4. Set roles and fine-grained access

access runs RBAC, ABAC and ReBAC on Cedar by default, or on the engine your customer already runs. See Permissions and Policy as code.

5. Keep an audit log

Every action and every decision is written to an append-only, tamper-evident audit log in OCSF.

main.yaml
logging:
  audit: true
  redact: [personal, sensitive, child, phi]
  retention: 12m

Streaming the audit log to each customer organisation's SIEM comes in phase 2. See Logs, audit and telemetry.

main.yaml
privacy:
  retention: { personal: 365d, logs: 30d, analytics: 13m }
  erase: { confirm-within: 30d }
ts
import { privacy } from "mainsdk";

declare const user: string;   // the person the hold is for

await privacy.hold(user, { until: "2027-06-30", reason: "Pinecart litigation hold" });
const record = await privacy.erase(user, { reason: "customer request" });   // signed erasure record

A legal hold pauses erasure and retention for that person and is audited. See Privacy and data rights.

7. Keep data in the customer's region

Overlays by market keep each customer's data in its region: US, EU or India. See Data and regions.

8. Restrict outbound network and show what the app does

Buyers ask which outside services your app can reach and how they can see it. Allow only named hosts, IPs and ranges, and export the same list as platform rules: see Restrict which hosts and IPs your app can reach. Send the audit log, decisions and destinations to the buyer's tools: see See every call, decision and change your app makes.

9. Verify it worked

bash
npx mainsdk check

The check passes only when every admin role signs in with a phishing-resistant method. Then sign in as a test user on a verified domain and confirm you are sent to the customer's identity provider.

Later: IP allowlists and customer-managed keys

Inbound IP allowlists per customer organisation, so only Pinecart's office and VPN ranges can sign in to Pinecart's workspace, run through limits. How they are configured is not decided yet: it arrives with the limits spec. See Rate limits and quotas.

Customer-held keys ship in phase 3 (milestone 1.2):

skip-test="customer-held keys ship in phase 3 (milestone 1.2) and are not built yet"
import { crypto } from "mainsdk";

await crypto.keys.customer("pinecart", {
  kms: "aws-xks",
  keyArn: "arn:aws:kms:eu-west-1:111122223333:key/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
});

Pinecart's records are then wrapped by Pinecart's own key, through AWS XKS or Google External Key Manager. If Pinecart revokes its key, reads of its data fail closed; other customers are unaffected.

What MainSDK doesn't cover

Private networking to the customer, on-premise hosting of the app and contractual SLAs belong to whoever runs the app, and mainsdk check says so.

Next

Open questions

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet