> ## Documentation Index
> Fetch the complete documentation index at: https://docs.air3.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Setting up your backend

> Deploy an AIR issuer backend with public HTTPS endpoints and add a database only when your implementation needs it.

Host an issuer backend so AIR can call its credential preview and issuance
endpoints. You can use a hosted service or expose a local service through an
HTTPS tunnel for sandbox development.

**A database is optional in sandbox.** Revocation, issuance history, and the
token status list need a database, so plan for one before production. Use any
database your backend supports; the reference issuer service uses PostgreSQL.
Encrypted credentials are stored in dStorage either way.

## What you are hosting

| Component | Purpose | Required? |
| - | - | - |
| Issuer backend | Authorizes issuance, loads claims, signs and encrypts credentials, uploads them to dStorage, and serves revocation status | Yes, for the on-demand backend integration |
| `did:web` document | Publishes the issuer DID and the keys verifiers use to check your credentials, at `/.well-known/did.json` | Yes; served by the issuer service |
| Public JWKS endpoint | Publishes the public keys AIR uses to validate Partner JWTs | Yes; it may share a host with your backend or web app |
| Database | Persists issuer history, revocation records, and the token status list. The reference issuer service uses PostgreSQL | Optional in sandbox; required for revocation |
| dStorage | Stores encrypted credential envelopes | Used by the issuance flow; independent of your application's database |

Credential claims can come from an existing service or API, an existing database,
or static test data. You do not need to create a new database simply to provide claims.

## Fork and review the backend implementation

Fork the SD-JWT branch of the [AIR issuer service](https://github.com/MocaNetwork/air-issuer-service/tree/main).
It issues `SD_JWT_VC` credentials under a `did:web` issuer DID.

* The service runs without `DATABASE_URL` for sandbox testing. For issuance history, revocation, and a token status list at scale across many users, you will need to persist the relevant data in your own database.
* Iden3 issuers use a separate branch. See [Iden3 credentials](/products/identity/iden3-credentials).

This guide follows the issuer service used in the
[issuance quickstart](/get-started/quickstarts/issue-credentials#step-1-generate-partner-secrets).

## Choose a hosting path

| Need | Options | Setup |
| - | - | - |
| Local sandbox | Local Node.js service with an HTTPS tunnel | Database only if your implementation or features require one |
| Hosted backend | Railway, Render, Koyeb, or your existing infrastructure | Public HTTPS, environment variables, and the service's build/start commands |
| Optional database | Any database your backend supports. For the reference issuer service: Railway PostgreSQL, Neon, Supabase, or your existing PostgreSQL service | For the reference issuer service, set `DATABASE_URL` and run migrations |

Check each provider's current plans and idle/sleep behavior before selecting a
sandbox host: [Railway](https://docs.railway.com/pricing/plans),
[Render](https://render.com/docs/free), [Koyeb](https://www.koyeb.com/docs/faqs/pricing),
[Neon](https://neon.com/docs/introduction/plans), and
[Supabase](https://supabase.com/docs/guides/platform/billing-on-supabase).
A sleeping service may delay the first credential request.

## Prerequisites

* Your issuer backend repository
* An [AIR Developer Dashboard](https://developers.sandbox.air3.com/dashboard) Partner ID
* Partner secrets configured using the [issuance quickstart](/get-started/quickstarts/issue-credentials#step-1-generate-partner-secrets)
* A public HTTPS domain or development tunnel

## Deploy the issuer service

The following steps use Railway as an example. Apply the same build commands,
environment configuration, and public endpoint requirements on another host.

<Steps>
  <Step title="Create a project and select your repository">
    Create a Railway project and add your fork of the AIR issuer service.
    Use the same revision you configured and tested locally.
  </Step>

  <Step title="Configure build and start commands">
    | Setting | Value |
    | - | - |
    | Build command | `npm i && npm run build` |
    | Start command | `npm run start:prod` |
    | Pre-deploy command | Leave empty unless you enabled database persistence |

    If you enable database persistence, set the pre-deploy command to
    `npx mikro-orm migration:up` after configuring `DATABASE_URL`.
  </Step>

  <Step title="Configure the public origin and environment">
    Generate a public HTTPS domain in the host's networking settings. Set
    `ISSUER_ORIGIN` to that origin without a trailing slash. Use a custom
    domain you control for production: the issuer DID is
    `did:web:<host of ISSUER_ORIGIN>`, so the host is your issuer identity.

    Add the environment values from
    [Configure and start the issuer backend](/get-started/quickstarts/issue-credentials#step-2-configure-and-start-the-issuer-backend),
    including Partner JWT key configuration, `PARTNER_JWKS`, and the issuer
    API keys. Keep secrets in the host's environment settings.

    For this sandbox example, use `NODE_ENV=sandbox` and sandbox API origins.
    Railway supplies `PORT`; the issuer service listens on it.
  </Step>

  <Step title="Optionally add a database">
    Skip this step if you are using a backend configuration without database
    persistence.

    The reference issuer service uses PostgreSQL. Add PostgreSQL to the project,
    or use an existing PostgreSQL service, and set `DATABASE_URL` on the issuer
    backend to its connection string. If you built your own backend, connect the
    database it uses instead.

    For a Railway database named `Postgres`, the reference variable is:

    ```text theme={null}
    DATABASE_URL=${{Postgres.DATABASE_URL}}
    ```

    Run migrations before starting the database-backed service. This enables the
    persistence used by issuance history, revocation records, and the token
    status list.
  </Step>

  <Step title="Deploy and check the service">
    Confirm the process starts successfully, then open
    `https://<your-host>/.well-known/did.json` and
    `https://<your-host>/.well-known/jwt-vc-issuer`. The DID document's `id` is
    the issuer DID you register with AIR. Verify that the public host routes
    requests to your issuer endpoints and that its TLS certificate is valid.

    When you use a database, also confirm connectivity and successful migrations.
    Use your implementation's health checks.
  </Step>
</Steps>

Keep `ISSUER_ORIGIN` stable once you issue credentials. It defines the issuer
DID, and revocation and status-list URLs in issued credentials refer to it.

## Optional database persistence

For the reference issuer service, apply its migrations when `DATABASE_URL` is configured:

```bash theme={null}
npx mikro-orm migration:up
```

Without database persistence, skip migrations and database readiness checks.
Do not use an empty database history as evidence that issuance failed, or rely
on persistent history and revocation records being available.

Connect a durable database and validate history and revocation behavior before
using the issuer with real users. dStorage keeps the encrypted credential; it
serves a different purpose from issuer application records. See
[Revoke credentials](/products/identity/revocation).

## After hosting: connect AIR

1. **Register the issuer.** Use **Issuer → Settings** in the sandbox or
   production Dashboard and enter the `did:web` issuer DID. Both environments
   are self-serve for SD-JWT issuers.
2. **Register the endpoint URLs.** Configure `availableVcApiUrl` and `issueVcApiUrl`
   to point to `POST /available-vc` and `POST /issue-vc` on your backend, and
   `revocationStatusApiUrl` to its `GET /revocation-status/:nonce` route.
3. **Match the issuer API key.** Register the backend's `API_KEY` with AIR.
   AIR sends it as `x-api-key`; keep it out of browser code.
4. **Register the public JWKS URL.** Its keys must match the Partner JWT signing
   key and `kid`. See [JWKS endpoint setup](/get-started/authentication/jwks-endpoint).
5. **Register the web app origin.** Add it under **Account → Domains**.
6. **Test issuance.** Confirm the holder sees the preview, approves issuance,
   and can use the resulting credential. See the
   [issuance quickstart](/get-started/quickstarts/issue-credentials#step-8-test-end-to-end).

The issuer endpoints and JWKS must be reachable by AIR over HTTPS. They may share
a domain; separate public origins for every component are not required.

If you enable CORS on the issuer or a reverse proxy in front of it, allow
`*.air3.com`. Restricting origins to your own domain breaks the holder claim flow.

## Go-live checklist

* [ ] `did:web` document served at `${ISSUER_ORIGIN}/.well-known/did.json` and its DID registered with AIR
* [ ] Partner JWT keys (`PARTNER_*`) match your JWKS, and `PARTNER_JWKS` holds the matching public keys
* [ ] Each Dashboard schema has a matching class in `src/issuer/sd-jwt-vc-schemas/`, exported from `index.ts`
* [ ] `generateCredentialData` returns schema-valid claims, and `disclosureFrame` and `expirySec` are set per schema
* [ ] The database is durable and its migrations are applied
* [ ] `ISSUER_ORIGIN` is public HTTPS on a domain you control, and the status endpoints are reachable
* [ ] `availableVcApiUrl`, `issueVcApiUrl`, `revocationStatusApiUrl`, and the API key are set in AIR
* [ ] If you use the [token status list](/products/identity/revocation#token-status-list), the partition size is final and a publish job is scheduled
* [ ] The claim flow passes end to end with a test holder

## Local development

Run the issuer locally and expose it through an HTTPS tunnel such as
[ngrok](https://ngrok.com) or
[cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/).
Follow the [HTTPS tunnel setup](/get-started/authentication/jwks-endpoint#local-development-https-tunnel).

Set `ISSUER_ORIGIN` and the registered issuer endpoint URLs to the tunnel address.
If the tunnel address changes, update both. Add a local or managed database
connection (PostgreSQL for the reference issuer service) only when your backend
configuration uses database persistence.

## Next steps

* [Credential issuance quickstart](/get-started/quickstarts/issue-credentials)
* [Revoke credentials](/products/identity/revocation)
* [JWKS endpoint setup](/get-started/authentication/jwks-endpoint)
* [Architecture and data flow](/technicals/architecture)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.