For the complete documentation index, see llms.txt. This page is also available as Markdown.

Bootstrap an Issuer Organization

Describe your organization in one file, apply it to staging, check it, then apply the same file to production.

An organization on LearnCard is an issuer profile, the signing authority that signs for it, optionally the profiles it manages (districts, campuses, chapters), and the API tokens your systems use to reach it. Set it up by hand once and you will set it up wrong the second time. Instead, describe it in a file and let the CLI make the network match.

~15 min · Needs Node 18+. Everything below runs against staging; the last step shows how the same file moves to production. Related: Who Signs Your Credentials? · Generate API Tokens · Test Safely

The one-line version

npx @learncard/cli org apply org.yaml --network staging --secrets-out ./secrets.env

Creates missing resources, updates supported profile and signing settings (display names, branding, primary signer, the SIGNING_AUTHORITY_* selection in .env), and leaves matching resources alone. Service-account scope or expiry drift requires revoking the grant and re-running; it is never silently updated. A self-hosted signer whose registered DID differs from the spec is reported as drift and fails the run. Unknown keys anywhere in the spec are rejected, so a typo cannot silently drop a section. With --secrets-out, a missing token is re-issued into that file. An identical run with all tokens present prints No changes.

Before you start: one folder, one identity

The CLI keeps your identity in a .env in the current folder. A folder is one profile on one network. Make a fresh folder for this guide, and a different folder (or let promote make one) for production.

mkdir my-org && cd my-org

1. Write the file

Start from the smallest example and grow it. Every example in packages/learn-card-cli/examples shows one idea:

File
Adds

minimal.network.yaml

An issuer profile that LearnCard signs for

service-account.network.yaml

A scoped, expiring API token for a backend that must not hold the seed

branded.network.yaml

Profile image, bios, website, and wallet display colours

self-hosted-signing.network.yaml

You keep the keys; LearnCard calls your signing endpoint

state-districts.network.yaml

A parent org with managed profiles underneath it, plus a default webhook

Here is the minimal one. Save it as org.yaml:

Change profileId to something nobody else will have picked. The full field list is in the CLI reference.

2. Preview, then apply

Every row says would-create, and nothing was written — not on the network, not in your folder. Now for real:

Run it once more. It should end with No changes. — that is the whole point.

3. Check it

whoami shows who this folder is. doctor checks identity, network, signing authority (it signs and verifies a throwaway credential), your did:web document, and whether credential refresh is on, then prints one fix line for anything red. It calls network services and optionally POSTs a webhook ping, but does not issue credentials to recipients or provision resources. The Trusted Registry check is a manual check reported as skipped.

4. Send something

Because a signing authority is registered, this signs through it (you'll see Signing through the registered signing authority "example-signer"). Add --no-template to sign with the local key instead.

5. Grow the file

Add a service account

Your backend should not hold the seed. Add a token with only the permissions it needs and an expiry:

The token is written to secrets.env (mode 0600, added to .gitignore) as NIGHTLY_ISSUER=…. It is never printed or stored elsewhere by the CLI. Your backend uses it as Authorization: Bearer … or initLearnCard({ apiToken }) and acts as your issuer profile within those scopes. Re-running org apply --secrets-out ./secrets.env re-issues a token if its key is missing from the file. If scopes or expiry differ, apply errors with a revoke command; revoke the grant and re-run to create its replacement. Dry-run reports this as drifted.

Add branding

Only the fields you list are compared, and display colours are merged — naming two colours never wipes a third set in the app. Images must be https:// URLs you host; the validator will tell you if you pass a local path.

Add managed profiles

For a parent organization with members underneath it — a state with districts, a university with departments:

With a learncard-hosted signer, org apply also creates and registers a hosted signing authority on each managed profile (shown as signingAuthority example-north/example-signer created). That is what lets an API token, which has no key of its own, sign as a district later. With a self-hosted signer you register one on each district yourself.

Managed profiles have no seed of their own. You act as one from the parent's folder:

The credential is signed with did:web:…:users:example-north — the district, not the parent. whoami lists every profile you can --as, and LEARNCARD_AS=example-north does the same for a whole shell session without persisting anything. Passing --profile-id example-north instead is an error: the folder's identity doesn't change.

When creating a new runnable script, send --as saves MANAGED_DID in .env and binds the script to that managed issuer and network. The script rejects a missing or changed MANAGED_DID. Existing scripts are not overwritten; the CLI warns that they may use a different issuer.

In your own code it is one option on initLearnCard. Export NETWORK_URL from your staging project's .env along with the seed; without it this snippet defaults to production:

getManagedProfiles() on the manager's did:web (shown by whoami) returns every managed DID if you need to loop.

Let a service account act as a district

Your backend probably holds a token, not the seed. A token is bound to the profile that minted it — so to issue as a district, the request says who it is acting as, and the network checks two things: that your issuer really manages that profile, and that the token was allowed to delegate.

Allow it in the spec, per account:

Omit actAs and the token cannot act as anyone — this is deny-by-default on purpose, so a leaked single-district token can never issue as the whole state. doctor and whoami both report what each token may act as.

Then in your backend, name the district on the call. The send below passes template, so the network signs through the hosted signing authority org apply registered on the district; if your spec uses a self-hosted signer, register one on the district first or send() fails with You must register a signing authority.

org.invoke.actAs(id) returns a new instance; org itself is unchanged. If you only ever act as one profile, initLearnCard({ apiKey, network: true, actAs: id }) sets it once. Under the hood this is a single request header, X-LearnCard-Act-As, so any HTTP client can do the same against the REST API. The token's scope is unchanged while acting — inbox:write as a district is still just inbox:write.

6. Move it to production

Staging and production are separate networks: profiles, tokens, signing authorities, and credentials do not carry over. Your seed does. So the production setup is the same file, applied again:

Every row is would-create; nothing is written anywhere. Show this to whoever approves the change. Then:

This makes a .learncard/production/ folder holding a production .env (seed and profile ID copied, nothing else), applies the spec there, writes fresh tokens to .learncard/production/secrets.env, runs doctor, and prints the list of what did not carry over. From then on, cd .learncard/production to operate production.

The production issuer's DID is did:web:network.learncard.com:users:<profileId>. If you issue credentials that will later be updated in place (managed credentials), every version must come from that same DID — so settle on the profile ID before the first real issuance.

Troubleshooting

You see
It means

This folder's .env is already the profile "x"; --profile-id y would not…

One folder, one identity. Use --as y if y is a managed profile, or a new folder.

--from … does not match this folder's network

promote must run from the folder that is on --from. whoami shows which network a folder is on.

Credential refresh isn't enabled on this network

Expected on some deployments. doctor reports it; nothing else is affected.

local files are not uploaded yet — host the image…

Branding images must be https:// URLs for now.

This API token may not act as "x". Grant actAs on the token.

Add actAs to that service account in the spec, then re-run org apply — it detects the drift and errors with a token --revoke <grantId> command; run that, then re-run org apply --secrets-out ./secrets.env to mint a replacement token.

You do not manage profile "x".

x isn't under your profileManager.managed, or you're acting from the wrong issuer.

Profile not found. Are you sure this person exists?

You sent to a profile ID that doesn't exist on this network.

Signing authority "x" is registered at … with DID …, but the spec declares …

The network keys a registration by name + DID, so a rotated key cannot be swapped in place. Register it under a new name or fix did in the spec.

Unrecognized key(s) in object

A field in the spec is misspelled or misplaced. Compare with the reference table.

… already holds a different SECURE_SEED

The promote target folder belongs to another identity. Move that .env aside or choose another target.

Another org apply is reconciling service accounts for …

Two applies overlapped on the same secrets file. Wait for the other to finish (remove a stale .secrets.env.lock only if none is running).

Last updated

Was this helpful?