# Migrate from Ninetailed

Move your experiences to Croct without changing your CMS.

Contentful acquired Ninetailed in August 2024 and sunset the standalone app on March 26, 2026. The capability now lives only inside the Contentful web app, so teams running Ninetailed on Sanity, Storyblok, Contentstack, or any other CMS need a different personalization layer.

The migration from Ninetailed to Croct does not require replatforming. Your CMS stays where it is, and your existing content becomes the fallback for every slot.

In this tutorial, you will map Ninetailed's concepts to ours, replace the Ninetailed components with slots, and rebuild your audiences, experiences, and experiments.

## How the concepts map

Most of Ninetailed's model transfers directly. The vocabulary is close enough that the mapping is usually the easiest part of the migration:

| Ninetailed                         | Croct                                                                                                           | Notes                                                           |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Audience                           | [Audience](/explanation/audience/introduction)                                                                  | Real-time evaluation                                            |
| Experience                         | [Experience](/explanation/experience/introduction)                                                              | Same concept: an audience plus the content it receives          |
| Baseline                           | [Slot default content](/explanation/content/slot-default-content)                                               | The baseline can be your existing CMS content                   |
| Variant                            | [Variant](/explanation/experiment)                                                                              | Same concept                                                    |
| Experiment                         | [Experiment](/explanation/experiment)                                                                           | Bayesian analysis on unsampled data                             |
| Personalize / Experience component | [Slot](/explanation/slot)                                                                                       | The component fetches from a slot rather than wrapping variants |
| Profile and traits                 | [User profile](/explanation/user-profiles/introduction) and [attributes](/explanation/user-profiles/attributes) | Built in, no external CDP required                              |
| `identify`                         | [Identification](/explanation/user-profiles/introduction#identification)                                        | Same purpose                                                    |
| Track call                         | [GoalCompleted](/reference/event/types/engagement/goal-completed) and other [events](/reference/event/overview) | Richer typed event catalog                                      |

The structural difference is where variants live. Ninetailed stored baseline and variant entries in your CMS and referenced audiences from them. We keep your CMS content as the fallback or default and resolve variants from our own content layer, so no audience references or variant entries are added to your schemas.

## Before you start

You need:

- A [Croct account](https://app.croct.com/signup) with a workspace and application set up.
- Access to your CMS and your frontend repository.
- A list of your old Ninetailed experiences, their audiences, and the pages they run on.

> **Take the inventory first**
>
> Most Ninetailed implementations used a fraction of the capability. Before writing any code, list your old experiences, the audience behind each one, and the elements each one changes. Teams routinely find that a dozen configured experiences come down to three that actually make sense right now.

## Migrate your experiences

The migration follows the same flow as any other experiment, with your Ninetailed inventory as the starting point.

### Integrate the SDK

Run the CLI in your project:

**Command to initialize your project**

```sh
croct init
```

The CLI detects your framework, installs the SDK, wires the provider, and generates the type definitions. This is additive: nothing changes on your site until you fetch a slot, so you can merge it safely before migrating any experience.

### Replace Experience components with slots

This is the main code change. A Ninetailed integration wrapped a component and passed it a baseline plus a list of variants, each referencing an audience:

**components/HomeHero.jsx**

```jsx
// Variants and audience references came from your CMS
<Experience
  {...baselineEntry} // Any props your `component` needs
  id={baselineEntry.sys.id} // The content ID of the BASELINE entry
  component={YourComponent} // What to use to render the selected variant
  experiences={mappedExperiences} // Array of mapped experiences
/>
```

With Croct, the component fetches from a slot and passes your CMS content as the fallback. For each area of the page you personalize, follow these steps:

1. Create a [component](/immersion/tutorials/get-started/content-management#create-the-component) and a [slot](/immersion/tutorials/get-started/content-management#create-the-slot).

2. Add the slot to your project:

   ```sh
   croct add slot
   ```

   The CLI [generates the type definitions](/reference/cli/type-generation) and [downloads the default content](/reference/cli/fallback-content) for use as an automatic fallback.

3. [Fetch the content](/immersion/tutorials/get-started/content-management#render-the-slot) where the component renders, passing your CMS content as the fallback:

   **components/HomeHero.jsx**

   ```jsx
   export default async function HomeHero() {
     const cmsHero = await cms.getHero();

     const {content} = await fetchContent('home-hero', {
       fallback: cmsHero,
     });

     return <Hero {...content} />;
   }
   ```

   If you prefer to use Croct to serve only the dynamic content, while your CMS serves both the default and the fallback, check where the content came from before rendering it. When the [metadata.contentSource](/reference/api/service/content/endpoint/client/content#metadata-contentsource-prop) value is `slot`, the request matched no experience or A/B test, so the result contains no dynamic content, and your CMS content should be rendered instead.

   **components/HomeHero.jsx**

   ```diff
   export default async function HomeHero() {
     const cmsHero = await cms.getHero();

     const {content, metadata} = await fetchContent('home-hero', {
       fallback: cmsHero,
     });

   +  // Render the Sanity content unless Croct returned dynamic content
   +  const hero = metadata?.contentSource === 'slot' ? cmsHero : content;

     return <Hero {...hero} />;
   }
   ```

Three things follow from this shape:

- **Your CMS query is unchanged.** No audience references, variant arrays, or segment parameters.
- **The fallback and, optionally, the default is your existing content.** When no experience matches, or if a request fails, the page renders exactly what it renders today.
- **Resolution happens on the server**, so the correct content is in the first HTML response. There is no client-side swap and no flicker.

### Move identification and tracking

Ninetailed's `identify` call becomes our [identification](/explanation/user-profiles/introduction#identification) feature and `identify` function. See the data collection guide for [your SDK](/reference/sdk) for details.

```js
croct.identify(userId);
```

For conversions, track a [`goalCompleted`](/reference/event/types/engagement/goal-completed) event where the conversion happens:

```jsx
croct.track('goalCompleted', {
  goalId: 'sign-up',
});
```

To fuel your experience dashboards, also track the [`leadGenerated`](/reference/event/types/engagement/lead-generated) and [`orderPlaced`](/reference/event/types/ecommerce/order-placed) events.

### Recreate your audiences

Ninetailed audiences are built from audience traits and behaviors in the audience builder, and Croct is no different. We evaluate [audiences](/explanation/audience/introduction) on the server against a live profile, so behavioral conditions reflect the current session rather than a value synced earlier.

### Rebuild experiences and experiments

Create an [experience](/explanation/experience/introduction) for each audience, attach the slots it should affect, and write the content for each variant.

> **Relaunch with experiments**
>
> Migrating is the cheapest opportunity you will ever have to check whether your personalization strategy was earning its keep. Rather than switching each experience back on and assuming it still works, run it against your default content as an [experiment](/explanation/experiment). Some experiences will turn out to have been neutral, and finding that out now costs nothing.

### Review and publish

Before you let the experiments run, confirm that everything behaves as expected:

1. **Preview each variant**

   Use [preview mode](/explanation/content/preview) to render the page as a member of each audience before publishing.

2. **Confirm that the content is in the HTML**

   View the page source, or disable JavaScript, and check that the personalized content is in the initial response.

3. **Confirm that goals are firing**

   Complete the conversion yourself and check that the event appears with the correct goal.

## What does not carry over

Keep these differences in mind when planning the work:

- **Variant entries in your CMS become unused** since we do not read them. Leave them in place during the migration, then clean up the audience references and variant entries once every experience is live on Croct.
- **Audience definitions are rewritten, not imported from Ninetailed.** In practice, audience criteria are quick to write, and the inventory usually reveals fewer audiences than expected.
- **Merge tags and inline personalization tokens** are configured differently. We resolve the whole content of a slot rather than substituting tokens in a string.

## Troubleshooting

| Symptom                                     | Likely cause                                                                                                                                                        |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| All visitors see the same content           | The route is statically generated. Confirm that the component calls `fetchContent` and renders dynamically. See [Prerendering](/reference/sdk/nextjs/prerendering). |
| Content flashes before updating             | The slot is rendering on the client. Move it to a server-side fetch.                                                                                                |
| Personalization does not match the audience | The CQL condition does not evaluate as expected. Test it in isolation with [`evaluate`](/reference/sdk/nextjs/api/functions/evaluate).                              |
| A returning visitor is treated as new       | The client ID is not persisting. Check that a proxy or CDN is not stripping the cookies.                                                                            |
| Experiences differ across devices           | Expected for anonymous visitors. [Identify the user](/explanation/user-profiles/identity-resolution) to link their profiles.                                        |
| Fallback content appears in production      | Content requests are failing. Check the application credentials and the `timeout` option.                                                                           |

## Explore

- [Server-side experiments](/immersion/tutorials/experiment-feature-flagging): Run AB tests on the same infrastructure.
- [Audiences](/explanation/audience/introduction): Discover what an audience is and how it works.
