---
title: "Make your app enterprise ready"
description: "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."
section: "guides"
canonical: "https://mainsdk.dev/docs/guides/enterprise-ready"
updated: "2026-10-28"
---

# 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.

Canonical: https://mainsdk.dev/docs/guides/enterprise-ready · 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](https://mainsdk.dev/docs/interfaces/auth).

## 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.

```yaml title="main.yaml"
auth:
  driver: github
  methods: [passkeys, sso]
  sso: { protocols: [saml, oidc], scim: true }
  admin: { roles: [admin, support], require: phishing-resistant }
```

```ts title="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](https://mainsdk.dev/docs/interfaces/auth#saml) 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

```ts title="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](https://mainsdk.dev/docs/interfaces/access) and [Policy as code](https://mainsdk.dev/docs/concepts/policy).

## 5. Keep an audit log

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

```yaml title="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](https://mainsdk.dev/docs/interfaces/log).

## 6. Set retention, deletion and legal hold

```yaml title="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](https://mainsdk.dev/docs/interfaces/privacy).

## 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](https://mainsdk.dev/docs/concepts/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](https://mainsdk.dev/docs/guides/restrict-network). Send the audit log, decisions and destinations to the buyer's tools: see [See every call, decision and change your app makes](https://mainsdk.dev/docs/guides/see-what-your-app-does).

## 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 [TBD]: it arrives with the `limits` spec. See [Rate limits and quotas](https://mainsdk.dev/docs/interfaces/limits).

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

```ts
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

- [Restrict which hosts and IPs your app can reach](https://mainsdk.dev/docs/guides/restrict-network)
- [Security model](https://mainsdk.dev/docs/concepts/security-model)
- [Policy as code](https://mainsdk.dev/docs/concepts/policy)
- [Free and Cloud](https://mainsdk.dev/docs/concepts/free-and-cloud)
