Back to templates

saas-users

SaaS Users

Use with destination tables shaped for product users, members, or account invites.

Let customers bring member lists into your product quickly without shipping a generic spreadsheet tool or a risky direct insert path.

Who this is for

Best for SaaS founders onboarding members, admins, or workspace users from an existing spreadsheet.

Sample import idea

A workspace admin imports users from a CSV, checks roles and created dates, repairs invalid rows, and sees a clear completion summary before moving on.

Sample headers

email, display_name, role, created_at, plan

Canonical fields

These are the typed fields the template already understands across the widget, validation rules, dashboard config, and playground.

Email

email

emailrequired

Display name

display_name

textrequired

Role

role

enumoptional

Created at

created_at

dateoptional

Plan

plan

enumoptional

Widget flow

The same sequence appears in the hosted widget and the public playground.

  1. 1

    Upload CSV or Excel

    Upload a spreadsheet and start the browser-side flow immediately.

  2. 2

    Confirm smart column mapping

    Confirm the suggested mapping instead of rebuilding a schema by hand.

  3. 3

    Review validation results

    See what fails clearly and repair bad values inline.

  4. 4

    Fix invalid values inline

    Keep real imports safe with server authority and quotas.

  5. 5

    Import with a clear summary

    End with a simple inserted vs failed summary customers can trust.

React drop-in example

After you create a kit, replace the example token with the public embed token from your kit.

'use client'

import { useEffect, useRef } from 'react'

const IMPORTFLOW_APP_ORIGIN = "https://your-importflow-domain.example"
const IMPORTFLOW_TOKEN = "if_pub_xxxxxxxxx"
const IMPORTFLOW_APPEARANCE = {} as const
const IMPORTFLOW_SCRIPT_ID = 'importflow-embed-loader'

type ImportFlowSummary = {
  importJobId: string
  requestedRows: number
  validRows: number
  insertedRows: number
  failedRows: number
  completedAt: string
}

type ImportFlowMount = {
  destroy: () => void
}

type ImportFlowGlobal = {
  mount: (options: {
    token: string
    target: HTMLElement
    theme?: 'light' | 'dark' | 'system'
    primaryColor?: string
    surfaceColor?: string
    textColor?: string
    radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
    onComplete?: (summary: ImportFlowSummary) => void
    onClose?: (event: { reason: string; phase: string }) => void
  }) => ImportFlowMount
}

declare global {
  interface Window {
    ImportFlow?: ImportFlowGlobal
  }
}

function clearTarget(target: HTMLElement) {
  target.replaceChildren()
}

function loadImportFlowScript() {
  if (window.ImportFlow) {
    return Promise.resolve()
  }

  const existing = document.getElementById(IMPORTFLOW_SCRIPT_ID) as HTMLScriptElement | null
  if (existing) {
    return new Promise<void>((resolve, reject) => {
      existing.addEventListener('load', () => resolve(), { once: true })
      existing.addEventListener('error', () => reject(new Error('ImportFlow failed to load.')), {
        once: true,
      })
    })
  }

  return new Promise<void>((resolve, reject) => {
    const script = document.createElement('script')
    script.id = IMPORTFLOW_SCRIPT_ID
    script.src = new URL('/embed/v1/importflow.js', IMPORTFLOW_APP_ORIGIN).toString()
    script.async = true
    script.addEventListener('load', () => resolve(), { once: true })
    script.addEventListener('error', () => reject(new Error('ImportFlow failed to load.')), {
      once: true,
    })
    document.head.appendChild(script)
  })
}

export function ImportFlowDropIn() {
  const targetRef = useRef<HTMLDivElement | null>(null)
  const mountRef = useRef<ImportFlowMount | null>(null)

  useEffect(() => {
    const target = targetRef.current
    if (!target) {
      return
    }

    let cancelled = false
    mountRef.current?.destroy()
    mountRef.current = null
    clearTarget(target)

    loadImportFlowScript()
      .then(() => {
        if (cancelled || !window.ImportFlow) {
          return
        }

        mountRef.current?.destroy()
        clearTarget(target)
        mountRef.current = window.ImportFlow.mount({
          token: IMPORTFLOW_TOKEN,
          target,
          ...IMPORTFLOW_APPEARANCE,
          onComplete(summary) {
            console.log('ImportFlow complete', summary)
          },
          onClose(event) {
            console.log('ImportFlow closed', event)
          },
        })
      })
      .catch((error: unknown) => {
        if (!cancelled) {
          console.error(error)
        }
      })

    return () => {
      cancelled = true
      mountRef.current?.destroy()
      mountRef.current = null
      clearTarget(target)
    }
  }, [])

  return <div ref={targetRef} />
}

JavaScript embed example

The same real hosted loader works without React by mounting into a target element.

<div id="importflow-widget"></div>
<script src="https://your-importflow-domain.example/embed/v1/importflow.js"></script>
<script>
  const importflowAppearance = {}
  const importflow = window.ImportFlow.mount({
    token: 'if_pub_xxxxxxxxx',
    target: '#importflow-widget',
    ...importflowAppearance,
    onReady(payload) {
      console.log('ImportFlow ready', payload)
    },
    onComplete(summary) {
      console.log('ImportFlow complete', summary)
    },
    onClose(event) {
      console.log('ImportFlow closed', event)
    },
  })

  // Optional cleanup if your app dynamically removes this view:
  // importflow.destroy()
</script>

See it before you wire it

Open the safe playground and try the full flow for SaaS Users.

The playground uses the same browser-side parsing, mapping, review, and inline correction experience while keeping completion safely local.