Canonical fields
These are the typed fields the template already understands across the widget, validation rules, dashboard config, and playground.
Full name
full_name
Department
department
Job title
job_title
Start date
start_date
Salary
salary
hr-employees
Use with destination tables shaped for employee rosters or staff directories.
Handle employee roster imports with the same guided upload, mapping, review, and correction flow customers expect from mature HR products.
Who this is for
Best for HR, people ops, and workforce-tool founders onboarding employee directories into a Supabase app.
Sample import idea
An HR manager uploads an employee spreadsheet, checks departments and dates, corrects a handful of rows inline, and imports the valid staff records safely.
Sample headers
full_name, email, department, job_title, start_date, salary
These are the typed fields the template already understands across the widget, validation rules, dashboard config, and playground.
Full name
full_name
Department
department
Job title
job_title
Start date
start_date
Salary
salary
The same sequence appears in the hosted widget and the public playground.
Upload CSV or Excel
Upload a spreadsheet and start the browser-side flow immediately.
Confirm smart column mapping
Confirm the suggested mapping instead of rebuilding a schema by hand.
Review validation results
See what fails clearly and repair bad values inline.
Fix invalid values inline
Keep real imports safe with server authority and quotas.
Import with a clear summary
End with a simple inserted vs failed summary customers can trust.
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} />
}
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
The playground uses the same browser-side parsing, mapping, review, and inline correction experience while keeping completion safely local.