A beautiful web interface for migrating data between CRM systems (Twenty.com
Built by Beton β’ Follow @stochasticmacaw
# Install dependencies
npm install
# Run the app
npm run devOpen http://localhost:3000 and start migrating!
No database required β the app works immediately with a simple demo user. Perfect for one-time migrations.
Add your Twenty.com or Attio API credentials through the clean UI. All credentials are stored securely (in memory for quick runs, or in Supabase for persistence).
- View all your CRM entities (companies, contacts, deals, etc.)
- See field names and sample data
- Select exactly what you want to migrate
- Execute migrations with real-time progress tracking
- Monitor batch processing with rate-limit controls
- View detailed logs for every operation
Configure a webhook URL to receive notifications when migrations complete. Perfect for integrating with Slack, Discord, or your own systems.
This is perfect for one-time migrations or testing:
# 1. Clone the repository
git clone https://github.com/getbeton/beton-trolley.git
cd beton-trolley
# 2. Install dependencies
npm install
# 3. Run the app
npm run devVisit http://localhost:3000 and you're ready to go!
Want to persist credentials and migration history? Add Supabase:
# 1. Copy the environment template
cp .env.example .env.local
# 2. Add your Supabase credentials to .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# 3. Run the SQL migration
# Go to Supabase β SQL Editor β paste contents of supabase-migration.sql
# 4. Start the app
npm run dev- Centralized Auth Flow (Supabase + Next.js middleware with encoded return state)
- Next.js 16 + TypeScript
- Tailwind CSS + shadcn/ui components
- tRPC for type-safe APIs
- Supabase (optional) for data persistence
- TanStack Query for data fetching
- Add your Twenty.com base URL and API token
- Add your Attio API token
- Select which entities (companies, contacts) to migrate
- Map fields between systems
- Run the migration and watch it complete in real-time
Use the built-in field mapping to identify and merge duplicate records before migration.
Run test migrations to validate your API credentials and field mappings before going to production.
Set up a webhook to get notified in Slack when long-running migrations finish:
# Just add your webhook URL in the UI
https://hooks.slack.com/services/YOUR/WEBHOOK/URLNEXT_PUBLIC_SUPABASE_AUTH_URLβ Shared auth project URL (points to beton-auth)NEXT_PUBLIC_SUPABASE_AUTH_ANON_KEYβ Shared anon key for auth project
NEXT_PUBLIC_SUPABASE_URLβ Your Supabase project URLNEXT_PUBLIC_SUPABASE_ANON_KEYβ Public anon keySUPABASE_SERVICE_ROLE_KEYβ Service role key (keep secret!)
MIGRATION_RATE_LIMIT_MSβ Delay between batches (default: 500ms)LOG_LEVELβ Set todebugfor verbose logging
The app needs API credentials for your CRM systems. Add these through the web UI:
Credentials are validated when you save them, and you'll see a green checkmark when they're working.
Want to get notified when migrations finish?
- Add a
NOTIFICATION_WEBHOOKcredential in the UI - Use any webhook URL (webhook.site, Slack, Discord, etc.)
- Migrations will automatically POST JSON payloads:
{
"migration": "CRM Migration",
"runId": "550e8400-...",
"status": "SUCCEEDED",
"progress": 100,
"recordsProcessed": 1500,
"startedAt": "2025-11-30T10:00:00Z",
"completedAt": "2025-11-30T10:05:00Z"
}Test your webhooks with the included utilities:
node test-webhook-receiver.js # Start local receiver
node test-webhook.js YOUR_URL # Send test notification- Visitors hit
https://trolley.getbeton.ai(or any other allowed app domain). The Next.js middleware checks the Supabase session using the shared beton-auth project. If there is no session, the middleware redirects the user to/signinon the same host while preserving the original destination in areturnquery parameter. - The
/signinpage callssupabase.auth.signInWithOAuth()and encodes the desired return URL inside the OAuthstateparameter. The callback URL always points to/auth/callbackon the originating domain. - Supabase completes the OAuth handshake and redirects the browser back to
/auth/callback. That route exchanges the authorization code for a session, validates the decodedstate.returnToURL against the whitelist insrc/lib/utils/domain.ts, and then redirects the user back to their original page (defaulting to/on trolley). - Because middleware sets Supabase cookies on the
.getbeton.aiparent domain in production, the session is immediately available to all Beton subdomains that host this app. There is no separate auth service to keep in syncβbothauth.getbeton.aiandtrolley.getbeton.aiserve the same Next.js deployment (seevercel.json), so maintenance is centralized.
This design eliminates brittle redirect cookies and still keeps the door open for additional Beton apps (e.g., auth.getbeton.ai, enrichment.getbeton.ai) by simply adding their hostnames to ALLOWED_RETURN_DOMAINS.
beton-trolley/
βββ src/
β βββ app/ # Next.js pages & API routes
β βββ components/ # React components
β βββ lib/ # Utilities & Supabase client
β βββ server/ # tRPC routers & migration engine
βββ public/ # Static assets
βββ supabase-migration.sql # Database schema
βββ README.md # You are here
We welcome contributions! Here's how:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run the build to verify (
npm run build) - Commit your changes (
git commit -m 'Add amazing feature') - Push to your branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Blog: Substack
- Twitter: @stochasticmacaw
MIT License - see LICENSE for details.
Beton builds tools for modern data teams. Learn more at getbeton.ai.
More from Beton:
- π Substack - Guides and best practices
- π§ GitHub - Open source tools
- π¦ Founder Twitter - Updates and announcements
Made with β€οΈ by the Beton team