# Get Started with Headless Config

In this tutorial, you run a complete [headless configuration](https://prismatic.io/docs/integrations/headless-configuration.md) example from start to finish. You publish a code-native integration called **Fake CRM**, configure it from your own app with a field mapper and a live preview, run its flow, and then publish an update.

The integration syncs people from Fake CRM, a stand-in for a Salesforce-like CRM, to contacts in Acme, your app. Each customer stores people differently: some use the standard **Lead** or **Contact** record types, and one customer, Hooli, uses a custom `Hooli_Leads__c` record type with its own field names. So each customer chooses a record type and maps its fields to Acme's contact fields.

Fake CRM's data is static, so the integration needs no connections or credentials.

## Before you begin[​](#before-you-begin "Direct link to Before you begin")

You need:

* A Prismatic organization with the headless configuration feature enabled. Contact your Prismatic representative to enable it.
* An admin account in that organization
* The [Prism CLI](https://prismatic.io/docs/cli.md) 10.6.0 or later, logged in with `prism login`
* [Bun](https://bun.sh) and [Node.js](https://nodejs.org) 22.19.0 or later, which Prism 11 requires
* A customer in your organization to sign in as. The example app signs in as an embedded user of this customer.

## Run the example[​](#run-the-example "Direct link to Run the example")

Clone the example

The example lives in Prismatic's [embedded repository](https://github.com/prismatic-io/embedded) on GitHub. It has two parts: the example embedded app, and the integration it configures.

```bash
git clone https://github.com/prismatic-io/embedded.git
cd embedded/examples/example-embedded-app

```

| Path                                                           | What it is                                                    |
| -------------------------------------------------------------- | ------------------------------------------------------------- |
| `integrations/fake-crm/`                                       | The Fake CRM code-native integration                          |
| `frontend/routes/examples/headless-configuration.tsx`          | The app page that lists integrations and configures instances |
| `frontend/components/headless/fake-crm-configuration-form.tsx` | The field mapper form                                         |

Read the integration's configuration

Open `integrations/fake-crm/src/configuration.ts`. Instead of config pages, the integration defines a **configuration** with these parts:

* A **schema**, written with zod, that describes what each instance saves: a record type, and the Fake CRM field each Acme contact field is read from.

  integrations/fake-crm/src/configuration.ts

  ```typescript
  export const configurationSchema = z.object({
    recordType: z.string().min(1).meta({ title: "Record type" }),
    fieldMapping: z
      .object({
        email: z.string().min(1).meta({ title: "Email" }),
        firstName: z.string().optional().meta({ title: "First name" }),
        lastName: z.string().min(1).meta({ title: "Last name" }),
        phone: z.string().optional().meta({ title: "Phone" }),
        company: z.string().optional().meta({ title: "Company" }),
        jobTitle: z.string().optional().meta({ title: "Job title" }),
      })
      .meta({ title: "Field mapping" }),
  });

  ```

* A **version**, `fake-crm-v1`, which names this shape of the schema.

* Three **server functions** the app calls while a customer configures an instance:

  * `listRecordTypes` returns the record types in the customer's CRM.
  * `listFields` returns the fields of one record type.
  * `getSampleRecord` returns a sample record and the Acme contact the customer's unsaved mapping makes from it.

* An **init** function that suggests starting values: the saved values when there are some, and a standard Lead mapping when there aren't.

Then open `integrations/fake-crm/src/flows.ts`. The **Sync Contacts to Acme** flow reads the saved values from `context.configuration`, maps each Fake CRM record to an Acme contact, and logs what it would send to Acme.

Import and publish the integration

Build the integration and import it into Prismatic:

```bash
cd integrations/fake-crm
bun install
bun run import

```

The command prints the new integration's ID. Publish the integration, which prints the ID of the published version, and make that version available in your marketplace:

```bash
prism integrations:publish <integration-id> --comment "First version"
prism integrations:marketplace <version-id> --available --deployable

```

You can also publish and manage marketplace availability in Prismatic's web app.

Configure and start the example app

Return to the `example-embedded-app` directory and follow the [app's setup instructions](https://github.com/prismatic-io/embedded/tree/main/examples/example-embedded-app#configure-the-app). You create a `.env.local` file with your organization and customer, and an RSA key pair so the app can sign embedded JWTs. The README lists every value the app requires. Set `PRISMATIC_ROLE` to `admin`, so the embedded user can activate integrations.

Then start the app:

```bash
cd ../..
bun install
bun run dev

```

Open <http://localhost:3000> and click **Headless Configuration** in the sidebar.

The page lists your marketplace with the app's own card components. Each card shows the integration's configuration experience: **headless** for Fake CRM, and **hosted** for integrations that use a config wizard.

Set up Fake CRM

1. On the **Fake CRM** card, click **Set up**, then click **Activate Fake CRM**. The app creates an instance that isn't deployed yet, and runs the integration's `init` function to get starting values.

2. Choose **Hooli Lead**, the custom record type, and click **Next: map fields**. The record types came from the `listRecordTypes` server function.

3. Map each Acme field to a `Hooli_Leads__c` field:

   * **Email** to **Primary Email**
   * **First name** to **Given Name**
   * **Last name** to **Family Name**
   * **Company** to **Organization**
   * **Job title** to **Job Title**

   Leave **Phone** set to **Don't sync**. The dropdown options come from the `listFields` server function.

4. Watch the **Preview in Acme** panel. Each time you change the mapping, the app sends the unsaved mapping to the `getSampleRecord` server function, and shows the Acme contact it makes from a sample Hooli lead.

5. Click **Save and activate**. The app saves the values, which Prismatic validates against the schema, and then deploys the instance.

The instance's status changes to **Active**, and the page shows the saved configuration.

Run the flow

The page shows a `curl` command with the flow's webhook URL. Run it:

```bash
curl -X POST 'https://hooks.prismatic.io/trigger/...'

```

In Prismatic, open your customer's **Fake CRM** instance and view its logs. The flow logged each Hooli lead as an Acme contact, using your mapping:

```text
Mapping 2 Hooli_Leads__c records to Acme contacts
Sending contact to Acme: {"email":"gavin.belson@hooli.example","firstName":"Gavin","lastName":"Belson","company":"Hooli","jobTitle":"Chief Executive Officer"}

```

Publish an update

Add a field to the configuration, and see how an update reaches the instance.

1. In `integrations/fake-crm/src/configuration.ts`, add an optional `department` field to `fieldMapping`:

   ```typescript
   department: z.string().optional().meta({ title: "Department" }),

   ```

   Add `"department"` to the list of field mapping keys in the `ui` layout, so the app knows where to show it.

2. In `integrations/fake-crm/src/acme.ts`, add the field to `ACME_CONTACT_FIELDS`, so the flow and the preview map it:

   ```typescript
   { key: "department", label: "Department", required: false },

   ```

3. Import, publish, and make the new version available, as before.

4. Back in the app, click **Manage** on the **Fake CRM** card. The page offers **Update to version 2**.

   The field is optional, so the instance's saved values still satisfy the schema. You kept the configuration version, `fake-crm-v1`, so the update doesn't require reconfiguration, and the app moves the instance to it with one click.

5. Click **Edit configuration**, then click **Next: map fields**. The new **Department** field appears in the mapper, although you didn't change the app. The form reads its fields from the configuration's schema.

If you made a change that saved values can't satisfy, such as renaming a field, you would also change the configuration version. The update would then require reconfiguration, and your `init` function would migrate the saved values for the customer to review. See [Configuration versions and init](https://prismatic.io/docs/integrations/headless-configuration/configuration-versions.md).

## What you learned[​](#what-you-learned "Direct link to What you learned")

* A headless integration defines a **schema**, a **version**, **server functions**, and an **init** function in place of config pages.
* Your app renders the configuration form with its own components, using [Solis](https://prismatic.io/docs/embed/headless.md), Prismatic's custom UI SDK.
* Server functions supply dynamic data, including previews of values the customer hasn't saved.
* Prismatic validates saved values, and flows read them with their real types from `context.configuration`.
* Keeping the configuration version lets instances update without reconfiguring.

## Next steps[​](#next-steps "Direct link to Next steps")

* [Configuration schema](https://prismatic.io/docs/integrations/headless-configuration/schema.md)
* [Server functions](https://prismatic.io/docs/integrations/headless-configuration/server-functions.md)
* [Building configuration UIs with Solis React](https://prismatic.io/docs/embed/headless/react.md)
* [Migrating from config pages](https://prismatic.io/docs/integrations/headless-configuration/migrating-from-config-pages.md)
