ARCY AI

Quickstart

Install arcy.js in your framework, tell ARCY who the user is, verify the install, sign user ids, follow sign-out and report key moments.

Your environment's snippets, with the real token already filled in, are in the dashboard under Settings > Installation. That page also carries the go-live steps for the environment you have selected; this page is the code. No account yet? Start with Create account.

Everything else ARCY does is configured in the dashboard, not in code. There is no config file and nothing to redeploy when you change a setting.

A coding agent can write all of this page in your repository for you: run Install arcy.js from ARCY MCP. It follows the steps below and asks only what your code does not answer.

Required steps

Add arcy.js to your Product

Pick your framework. Every path does the same two things: init() starts ARCY with your environment token, and identify() tells ARCY who the current user is.

Install the package:

Terminal
npm install arcy.js

Call ARCY once, where your Product starts. In a Vite or Create React App project that is your root component:

src/App.tsx
import { useEffect } from "react"
import { supabase } from "./supabase"
import arcy from "arcy.js"

export default function App() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const user = session?.user
      if (!user) return
      arcy.identify(user.id, {
        user_first_name: user.user_metadata.first_name,
        user_last_name: user.user_metadata.last_name,
        user_email: user.email,
        user_signed_up_at: user.created_at,
      })
    })

    return () => data.subscription.unsubscribe()
  }, [])

  return <YourApp />
}

The name keys come from user_metadata, so use whatever keys you store there.

src/App.tsx
import { useEffect } from "react"
import { useUser } from "@clerk/clerk-react"
import arcy from "arcy.js"

export default function App() {
  const { isLoaded, isSignedIn, user } = useUser()

  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
  }, [])

  useEffect(() => {
    if (!isLoaded || !isSignedIn) return
    arcy.identify(user.id, {
      user_first_name: user.firstName,
      user_last_name: user.lastName,
      user_email: user.primaryEmailAddress?.emailAddress,
      user_signed_up_at: user.createdAt?.toISOString(),
    })
  }, [isLoaded, isSignedIn, user])

  return <YourApp />
}
src/App.tsx
import { useEffect } from "react"
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"

export default function App() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    return onAuthStateChanged(getAuth(), (user) => {
      if (!user) return
      arcy.identify(user.uid, {
        user_first_name: user.displayName,
        user_email: user.email,
        user_signed_up_at: user.metadata.creationTime
          ? new Date(user.metadata.creationTime).toISOString()
          : undefined,
      })
    })
  }, [])

  return <YourApp />
}

Firebase keeps one display name, so there is no separate last name to send.

src/App.tsx
import { useEffect } from "react"
import arcy from "arcy.js"

export default function App() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
    arcy.identify("USER_ID", {
      user_first_name: "USER_FIRST_NAME",
      user_last_name: "USER_LAST_NAME",
      user_email: "USER_EMAIL",
      user_signed_up_at: "USER_SIGNED_UP_AT",
    })
  }, [])

  return <YourApp />
}

Install the package:

Terminal
npm install arcy.js

arcy.js runs in the browser only, so keep the call in a client component.

Add one client component and render it in your root layout:

app/arcy.tsx
"use client"

import { useEffect } from "react"
import { supabase } from "@/lib/supabase"
import arcy from "arcy.js"

export function ARCY() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const user = session?.user
      if (!user) return
      arcy.identify(user.id, {
        user_first_name: user.user_metadata.first_name,
        user_last_name: user.user_metadata.last_name,
        user_email: user.email,
        user_signed_up_at: user.created_at,
      })
    })

    return () => data.subscription.unsubscribe()
  }, [])

  return null
}

The name keys come from user_metadata, so use whatever keys you store there.

app/arcy.tsx
"use client"

import { useEffect } from "react"
import { useUser } from "@clerk/nextjs"
import arcy from "arcy.js"

export function ARCY() {
  const { isLoaded, isSignedIn, user } = useUser()

  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
  }, [])

  useEffect(() => {
    if (!isLoaded || !isSignedIn) return
    arcy.identify(user.id, {
      user_first_name: user.firstName,
      user_last_name: user.lastName,
      user_email: user.primaryEmailAddress?.emailAddress,
      user_signed_up_at: user.createdAt?.toISOString(),
    })
  }, [isLoaded, isSignedIn, user])

  return null
}
app/arcy.tsx
"use client"

import { useEffect } from "react"
import { useSession } from "next-auth/react"
import arcy from "arcy.js"

export function ARCY() {
  const { data: session, status } = useSession()

  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
  }, [])

  useEffect(() => {
    if (status !== "authenticated" || !session.user.id) return
    arcy.identify(session.user.id, {
      user_first_name: session.user.name,
      user_email: session.user.email,
    })
  }, [status, session])

  return null
}

session.user.id is there only if your session callback puts it there.

app/arcy.tsx
"use client"

import { useEffect } from "react"
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"

export function ARCY() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    return onAuthStateChanged(getAuth(), (user) => {
      if (!user) return
      arcy.identify(user.uid, {
        user_first_name: user.displayName,
        user_email: user.email,
        user_signed_up_at: user.metadata.creationTime
          ? new Date(user.metadata.creationTime).toISOString()
          : undefined,
      })
    })
  }, [])

  return null
}

Firebase keeps one display name, so there is no separate last name to send.

app/arcy.tsx
"use client"

import { useEffect } from "react"
import arcy from "arcy.js"

export function ARCY() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
    arcy.identify("USER_ID", {
      user_first_name: "USER_FIRST_NAME",
      user_last_name: "USER_LAST_NAME",
      user_email: "USER_EMAIL",
      user_signed_up_at: "USER_SIGNED_UP_AT",
    })
  }, [])

  return null
}
app/layout.tsx
import { ARCY } from "./arcy"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <ARCY />
      </body>
    </html>
  )
}

Call ARCY once in _app.tsx, the component that wraps every page:

pages/_app.tsx
import { useEffect } from "react"
import { supabase } from "@/lib/supabase"
import arcy from "arcy.js"
import type { AppProps } from "next/app"

export default function App({ Component, pageProps }: AppProps) {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const user = session?.user
      if (!user) return
      arcy.identify(user.id, {
        user_first_name: user.user_metadata.first_name,
        user_last_name: user.user_metadata.last_name,
        user_email: user.email,
        user_signed_up_at: user.created_at,
      })
    })

    return () => data.subscription.unsubscribe()
  }, [])

  return <Component {...pageProps} />
}

The name keys come from user_metadata, so use whatever keys you store there.

pages/_app.tsx
import { useEffect } from "react"
import { useUser } from "@clerk/nextjs"
import arcy from "arcy.js"
import type { AppProps } from "next/app"

export default function App({ Component, pageProps }: AppProps) {
  const { isLoaded, isSignedIn, user } = useUser()

  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
  }, [])

  useEffect(() => {
    if (!isLoaded || !isSignedIn) return
    arcy.identify(user.id, {
      user_first_name: user.firstName,
      user_last_name: user.lastName,
      user_email: user.primaryEmailAddress?.emailAddress,
      user_signed_up_at: user.createdAt?.toISOString(),
    })
  }, [isLoaded, isSignedIn, user])

  return <Component {...pageProps} />
}
pages/_app.tsx
import { useEffect } from "react"
import { useSession } from "next-auth/react"
import arcy from "arcy.js"
import type { AppProps } from "next/app"

export default function App({ Component, pageProps }: AppProps) {
  const { data: session, status } = useSession()

  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
  }, [])

  useEffect(() => {
    if (status !== "authenticated" || !session.user.id) return
    arcy.identify(session.user.id, {
      user_first_name: session.user.name,
      user_email: session.user.email,
    })
  }, [status, session])

  return <Component {...pageProps} />
}

session.user.id is there only if your session callback puts it there.

pages/_app.tsx
import { useEffect } from "react"
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"
import type { AppProps } from "next/app"

export default function App({ Component, pageProps }: AppProps) {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    return onAuthStateChanged(getAuth(), (user) => {
      if (!user) return
      arcy.identify(user.uid, {
        user_first_name: user.displayName,
        user_email: user.email,
        user_signed_up_at: user.metadata.creationTime
          ? new Date(user.metadata.creationTime).toISOString()
          : undefined,
      })
    })
  }, [])

  return <Component {...pageProps} />
}

Firebase keeps one display name, so there is no separate last name to send.

pages/_app.tsx
import { useEffect } from "react"
import arcy from "arcy.js"
import type { AppProps } from "next/app"

export default function App({ Component, pageProps }: AppProps) {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
    arcy.identify("USER_ID", {
      user_first_name: "USER_FIRST_NAME",
      user_last_name: "USER_LAST_NAME",
      user_email: "USER_EMAIL",
      user_signed_up_at: "USER_SIGNED_UP_AT",
    })
  }, [])

  return <Component {...pageProps} />
}

Install the package:

Terminal
npm install arcy.js

Call ARCY where you create the Vue application:

src/main.js
import { createApp } from "vue"
import { supabase } from "./supabase"
import arcy from "arcy.js"
import App from "./App.vue"

createApp(App).mount("#app")

arcy.init("YOUR_ENVIRONMENT_TOKEN")

supabase.auth.getUser().then(({ data: { user } }) => {
  if (!user) return
  arcy.identify(user.id, {
    user_first_name: user.user_metadata.first_name,
    user_last_name: user.user_metadata.last_name,
    user_email: user.email,
    user_signed_up_at: user.created_at,
  })
})

The name keys come from user_metadata, so use whatever keys you store there.

src/main.js
import { createApp } from "vue"
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"
import App from "./App.vue"

createApp(App).mount("#app")

arcy.init("YOUR_ENVIRONMENT_TOKEN")

onAuthStateChanged(getAuth(), (user) => {
  if (!user) return
  arcy.identify(user.uid, {
    user_first_name: user.displayName,
    user_email: user.email,
    user_signed_up_at: user.metadata.creationTime
      ? new Date(user.metadata.creationTime).toISOString()
      : undefined,
  })
})

Firebase keeps one display name, so there is no separate last name to send.

src/main.js
import { createApp } from "vue"
import arcy from "arcy.js"
import App from "./App.vue"

createApp(App).mount("#app")

arcy.init("YOUR_ENVIRONMENT_TOKEN")
arcy.identify("USER_ID", {
  user_first_name: "USER_FIRST_NAME",
  user_last_name: "USER_LAST_NAME",
  user_email: "USER_EMAIL",
  user_signed_up_at: "USER_SIGNED_UP_AT",
})

Install the package:

Terminal
npm install arcy.js

Add a client plugin. The .client in the file name keeps it out of the server render:

plugins/arcy.client.js
import { supabase } from "~/lib/supabase"
import arcy from "arcy.js"

export default defineNuxtPlugin(() => {
  arcy.init("YOUR_ENVIRONMENT_TOKEN")

  supabase.auth.getUser().then(({ data: { user } }) => {
    if (!user) return
    arcy.identify(user.id, {
      user_first_name: user.user_metadata.first_name,
      user_last_name: user.user_metadata.last_name,
      user_email: user.email,
      user_signed_up_at: user.created_at,
    })
  })
})

The name keys come from user_metadata, so use whatever keys you store there.

plugins/arcy.client.js
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"

export default defineNuxtPlugin(() => {
  arcy.init("YOUR_ENVIRONMENT_TOKEN")

  onAuthStateChanged(getAuth(), (user) => {
    if (!user) return
    arcy.identify(user.uid, {
      user_first_name: user.displayName,
      user_email: user.email,
      user_signed_up_at: user.metadata.creationTime
        ? new Date(user.metadata.creationTime).toISOString()
        : undefined,
    })
  })
})

Firebase keeps one display name, so there is no separate last name to send.

plugins/arcy.client.js
import arcy from "arcy.js"

export default defineNuxtPlugin(() => {
  arcy.init("YOUR_ENVIRONMENT_TOKEN")
  arcy.identify("USER_ID", {
    user_first_name: "USER_FIRST_NAME",
    user_last_name: "USER_LAST_NAME",
    user_email: "USER_EMAIL",
    user_signed_up_at: "USER_SIGNED_UP_AT",
  })
})

Install the package:

Terminal
npm install arcy.js

Call ARCY from your root layout, inside onMount so it runs in the browser only:

src/routes/+layout.svelte
<script>
  import { onMount } from "svelte"
  import { supabase } from "./supabase"
  import arcy from "arcy.js"

  onMount(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    supabase.auth.getUser().then(({ data: { user } }) => {
      if (!user) return
      arcy.identify(user.id, {
        user_first_name: user.user_metadata.first_name,
        user_last_name: user.user_metadata.last_name,
        user_email: user.email,
        user_signed_up_at: user.created_at,
      })
    })
  })
</script>

<slot />

The name keys come from user_metadata, so use whatever keys you store there.

src/routes/+layout.svelte
<script>
  import { onMount } from "svelte"
  import { getAuth, onAuthStateChanged } from "firebase/auth"
  import arcy from "arcy.js"

  onMount(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    onAuthStateChanged(getAuth(), (user) => {
      if (!user) return
      arcy.identify(user.uid, {
        user_first_name: user.displayName,
        user_email: user.email,
        user_signed_up_at: user.metadata.creationTime
          ? new Date(user.metadata.creationTime).toISOString()
          : undefined,
      })
    })
  })
</script>

<slot />

Firebase keeps one display name, so there is no separate last name to send.

src/routes/+layout.svelte
<script>
  import { onMount } from "svelte"
  import arcy from "arcy.js"

  onMount(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
    arcy.identify("USER_ID", {
      user_first_name: "USER_FIRST_NAME",
      user_last_name: "USER_LAST_NAME",
      user_email: "USER_EMAIL",
      user_signed_up_at: "USER_SIGNED_UP_AT",
    })
  })
</script>

<slot />

Install the package:

Terminal
npm install arcy.js

Call ARCY from your root component:

src/app/app.component.ts
import { Component, OnInit } from "@angular/core"
import { supabase } from "./supabase"
import arcy from "arcy.js"

@Component({ selector: "app-root", templateUrl: "./app.component.html" })
export class AppComponent implements OnInit {
  ngOnInit() {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    supabase.auth.getUser().then(({ data: { user } }) => {
      if (!user) return
      arcy.identify(user.id, {
        user_first_name: user.user_metadata.first_name,
        user_last_name: user.user_metadata.last_name,
        user_email: user.email,
        user_signed_up_at: user.created_at,
      })
    })
  }
}

The name keys come from user_metadata, so use whatever keys you store there.

src/app/app.component.ts
import { Component, OnInit } from "@angular/core"
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"

@Component({ selector: "app-root", templateUrl: "./app.component.html" })
export class AppComponent implements OnInit {
  ngOnInit() {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    onAuthStateChanged(getAuth(), (user) => {
      if (!user) return
      arcy.identify(user.uid, {
        user_first_name: user.displayName,
        user_email: user.email,
        user_signed_up_at: user.metadata.creationTime
          ? new Date(user.metadata.creationTime).toISOString()
          : undefined,
      })
    })
  }
}

Firebase keeps one display name, so there is no separate last name to send.

src/app/app.component.ts
import { Component, OnInit } from "@angular/core"
import arcy from "arcy.js"

@Component({ selector: "app-root", templateUrl: "./app.component.html" })
export class AppComponent implements OnInit {
  ngOnInit() {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
    arcy.identify("USER_ID", {
      user_first_name: "USER_FIRST_NAME",
      user_last_name: "USER_LAST_NAME",
      user_email: "USER_EMAIL",
      user_signed_up_at: "USER_SIGNED_UP_AT",
    })
  }
}

Install the package:

Terminal
npm install arcy.js

Add a script to the layout every page uses. Astro bundles it and runs it in the browser:

src/layouts/Layout.astro
<slot />

<script>
  import { supabase } from "../lib/supabase"
  import arcy from "arcy.js"

  arcy.init("YOUR_ENVIRONMENT_TOKEN")

  supabase.auth.getUser().then(({ data: { user } }) => {
    if (!user) return
    arcy.identify(user.id, {
      user_first_name: user.user_metadata.first_name,
      user_last_name: user.user_metadata.last_name,
      user_email: user.email,
      user_signed_up_at: user.created_at,
    })
  })
</script>

The name keys come from user_metadata, so use whatever keys you store there.

src/layouts/Layout.astro
<slot />

<script>
  import { getAuth, onAuthStateChanged } from "firebase/auth"
  import arcy from "arcy.js"

  arcy.init("YOUR_ENVIRONMENT_TOKEN")

  onAuthStateChanged(getAuth(), (user) => {
    if (!user) return
    arcy.identify(user.uid, {
      user_first_name: user.displayName,
      user_email: user.email,
      user_signed_up_at: user.metadata.creationTime
        ? new Date(user.metadata.creationTime).toISOString()
        : undefined,
    })
  })
</script>

Firebase keeps one display name, so there is no separate last name to send.

src/layouts/Layout.astro
<slot />

<script>
  import arcy from "arcy.js"

  arcy.init("YOUR_ENVIRONMENT_TOKEN")
  arcy.identify("USER_ID", {
    user_first_name: "USER_FIRST_NAME",
    user_last_name: "USER_LAST_NAME",
    user_email: "USER_EMAIL",
    user_signed_up_at: "USER_SIGNED_UP_AT",
  })
</script>

Install the package:

Terminal
npm install arcy.js

Call ARCY from your root route, inside an effect so it runs after hydration:

app/root.tsx
import { useEffect } from "react"
import { supabase } from "./supabase"
import arcy from "arcy.js"

export default function App() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const user = session?.user
      if (!user) return
      arcy.identify(user.id, {
        user_first_name: user.user_metadata.first_name,
        user_last_name: user.user_metadata.last_name,
        user_email: user.email,
        user_signed_up_at: user.created_at,
      })
    })

    return () => data.subscription.unsubscribe()
  }, [])

  return <Outlet />
}

The name keys come from user_metadata, so use whatever keys you store there.

app/root.tsx
import { useEffect } from "react"
import { useUser } from "@clerk/react-router"
import arcy from "arcy.js"

export default function App() {
  const { isLoaded, isSignedIn, user } = useUser()

  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
  }, [])

  useEffect(() => {
    if (!isLoaded || !isSignedIn) return
    arcy.identify(user.id, {
      user_first_name: user.firstName,
      user_last_name: user.lastName,
      user_email: user.primaryEmailAddress?.emailAddress,
      user_signed_up_at: user.createdAt?.toISOString(),
    })
  }, [isLoaded, isSignedIn, user])

  return <Outlet />
}
app/root.tsx
import { useEffect } from "react"
import { getAuth, onAuthStateChanged } from "firebase/auth"
import arcy from "arcy.js"

export default function App() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")

    return onAuthStateChanged(getAuth(), (user) => {
      if (!user) return
      arcy.identify(user.uid, {
        user_first_name: user.displayName,
        user_email: user.email,
        user_signed_up_at: user.metadata.creationTime
          ? new Date(user.metadata.creationTime).toISOString()
          : undefined,
      })
    })
  }, [])

  return <Outlet />
}

Firebase keeps one display name, so there is no separate last name to send.

app/root.tsx
import { useEffect } from "react"
import arcy from "arcy.js"

export default function App() {
  useEffect(() => {
    arcy.init("YOUR_ENVIRONMENT_TOKEN")
    arcy.identify("USER_ID", {
      user_first_name: "USER_FIRST_NAME",
      user_last_name: "USER_LAST_NAME",
      user_email: "USER_EMAIL",
      user_signed_up_at: "USER_SIGNED_UP_AT",
    })
  }, [])

  return <Outlet />
}

No package to install. Use this path when you have no bundler, or when you install through a tag manager. Paste the block from the Installation page into your HTML just before the closing body tag. The first script is ARCY's loader: it already carries your environment's token, loads asynchronously, queues calls made before it finishes, and never needs editing. The second script is yours.

html
<script>
  supabase.auth.getUser().then(({ data: { user } }) => {
    if (!user) return
    arcy.identify(user.id, {
      user_first_name: user.user_metadata.first_name,
      user_last_name: user.user_metadata.last_name,
      user_email: user.email,
      user_signed_up_at: user.created_at,
    })
  })
</script>

This uses the Supabase client your page already creates, so keep that script above it.

html
<script>
  firebase.auth().onAuthStateChanged((user) => {
    if (!user) return
    arcy.identify(user.uid, {
      user_first_name: user.displayName,
      user_email: user.email,
      user_signed_up_at: user.metadata.creationTime
        ? new Date(user.metadata.creationTime).toISOString()
        : undefined,
    })
  })
</script>

This is the Firebase SDK you get from a script tag, so keep your Firebase scripts above it.

html
<script>
  arcy.identify("USER_ID", {
    user_first_name: "USER_FIRST_NAME",
    user_last_name: "USER_LAST_NAME",
    user_email: "USER_EMAIL",
    user_signed_up_at: "USER_SIGNED_UP_AT",
  })
</script>

Never put the HTML snippet and the package on the same page. Pick one.

The token in init() belongs to one environment. It is public and appears in your page source, so it is safe to ship. Each environment has its own token; switch environments in the dashboard sidebar to get another one.

Replace the placeholders

Only the Your own auth tab leaves placeholders behind. Clerk, Auth.js, Supabase and Firebase read the user out of their own session, so if you picked one of those go straight to the next step.

Swap each placeholder for a real, dynamic value from your session code, or every visitor arrives as the same person:

PlaceholderMeaning
USER_IDThe signed-in user's ID in your own database
USER_FIRST_NAMEThe user's first name, as a dynamic value
USER_LAST_NAMEThe user's last name, as a dynamic value
USER_EMAILThe user's real email, as a dynamic value
USER_SIGNED_UP_ATWhen the user signed up. ISO 8601, e.g. 2019-12-11T12:34:56Z

Verify the installation

Open the real page in a browser. Settings > Installation watches for traffic from the selected environment on its own, confirms it when it arrives, and names the failure when none does. Each verdict has a fix on Troubleshooting.

Optional steps

Add custom attributes

The attributes you pass to identify() decide what ARCY can work out. Pass plan_value and plan_cycle for revenue at risk and organization_id for per-account totals. Every key must be declared under Configure > Attributes before you send it; an undeclared key is dropped, never stored.

Sign user ids for identity verification

Without it, anyone can type arcy.identify("someone-elses-id") into a browser console and read that person's conversations. Your server signs each user id with the environment's Secret, and the browser passes the signature along. Where the Secret is shown and what replacing it does is on Environments.

The Secret is a server-side credential. Never put it in front-end code, a build variable that reaches the browser, a mobile binary, or a repository. Anyone holding it can sign any user id.

The signature is HMAC-SHA256 of the user id, keyed with the Secret, hex encoded. It covers the user id only, not the attributes, and it is the recipe Intercom and Segment use. Sign the exact string you pass to identify(): if your ids are integers and you call arcy.identify(String(user.id)), sign String(user.id).

Sign with the Secret of the environment whose token the page runs on: ARCY_SECRET beside ARCY_TOKEN, ARCY_DEV_SECRET beside ARCY_DEV_TOKEN. When the Secret is not set yet, send no userHash; the call still works while Require identity verification is off.

js
import { createHmac } from "node:crypto"

const userHash = createHmac("sha256", process.env.ARCY_SECRET)
  .update(String(user.id))
  .digest("hex")
python
import hashlib, hmac, os

user_hash = hmac.new(
    os.environ["ARCY_SECRET"].encode("utf-8"),
    str(user.id).encode("utf-8"),
    hashlib.sha256,
).hexdigest()
ruby
user_hash = OpenSSL::HMAC.hexdigest("SHA256", ENV["ARCY_SECRET"], user.id.to_s)
php
$userHash = hash_hmac('sha256', (string) $userId, getenv('ARCY_SECRET'));

Pass the result in the third argument of identify():

js
arcy.identify("USER_ID", {
  user_first_name: "USER_FIRST_NAME",
  user_email: "USER_EMAIL",
}, {
  userHash: "USER_HASH", // computed on your server, never in the browser
})

identifyAnonymous() and updateUser() take no hash. Until you switch Require identity verification on, signatures are checked and recorded but nothing is turned away. Watch the checks on Identity verification and switch it on once they pass.

Follow sign-in, profile changes and sign-out

Call ARCY from one place that watches your current user, never from each sign-out button or profile form, so a path added later is not missed:

Your current userCall
A user id you have not sent yetidentify(id, attributes, { userHash })
The same id, with a changed plan or roleupdateUser(changedAttributes)
No user, after there was onereset()
No user from the startidentifyAnonymous()

Without reset(), the next person on a shared browser inherits the last user's identity and conversation. On a plain HTML site, call identify() or identifyAnonymous() on each page load and put reset() on your sign-out link, before it navigates.

Report key moments

Call arcy.track(name) where your Product knows a milestone succeeded: signup finished, first project created, invite sent, checkout paid. Call it after the success, not on the click that starts it. A flow's completion event, a Target and a Checklist item can end on the name, so spell it exactly as the dashboard shows it.

js
arcy.track("project_created")

Serve visitors who are not signed in

For public pages, call arcy.identifyAnonymous() instead of identify(). The visitor gets an id in a first-party cookie on your domain, so a returning visitor is the same person across sessions and subdomains. An anonymous visitor's chat answers spend the same AI credits as an identified user's; cap them under Settings > Limits.

Installing through a tag manager

The HTML path works unchanged in Google Tag Manager and similar tools:

  1. Create a Custom HTML tag.
  2. Paste the full HTML snippet from the Installation page, both scripts included.
  3. Trigger it on all pages and publish the container.

The loader ignores a second initialization, so a tag that fires twice does no harm. Electron and self-hosting the loader are not supported; an Electron renderer can use the npm path.

See also

On this page