Migrating an Existing Calimatic App
Migration runbook for moving an existing Calimatic application with users and organizations to the Calimatic Auth platform.
This guide is for Category 2 apps: existing Calimatic products (e.g., EdTech, Enterprise hubs) that already have users and organizations in their own database and need to migrate to centralized authentication.
Prerequisites
- Admin access to the Calimatic Auth platform
- Read access to the legacy application's user database
- A registered app client for your application (see New Calimatic App)
- API key or app client credentials with
org:users:managepermission - A staging environment for testing the migration before production
Migration Overview
Phase 1: Prepare Phase 2: Dual Auth Phase 3: Migrate Phase 4: Cutover
~~~~~~~~~~~~~ ~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
Audit users Run legacy + OIDC Bulk import users Disable legacy
Map data fields in parallel Verify migration Remove old auth
Register app client Test with pilot group Send reset emails Monitor
Set up OIDC Assign licenses
1. Pre-Migration Checklist
Audit Existing Users
Query your legacy database to understand the scope:
-- Count total users
SELECT COUNT(*) as total_users FROM Users WHERE IsActive = 1;
-- Count by role
SELECT RoleId, COUNT(*) as count FROM Users WHERE IsActive = 1 GROUP BY RoleId;
-- Find duplicate emails
SELECT Email, COUNT(*) as count FROM Users
GROUP BY Email HAVING COUNT(*) > 1;
-- Find users without emails (cannot migrate)
SELECT COUNT(*) FROM Users WHERE Email IS NULL OR Email = '';
-- Last login distribution (helps prioritize)
SELECT
CASE
WHEN LastLoginDate > DATEADD(day, -30, GETDATE()) THEN 'Active (30d)'
WHEN LastLoginDate > DATEADD(day, -90, GETDATE()) THEN 'Active (90d)'
WHEN LastLoginDate > DATEADD(day, -365, GETDATE()) THEN 'Inactive (1yr)'
ELSE 'Dormant'
END as activity_bucket,
COUNT(*) as count
FROM Users
WHERE IsActive = 1
GROUP BY
CASE
WHEN LastLoginDate > DATEADD(day, -30, GETDATE()) THEN 'Active (30d)'
WHEN LastLoginDate > DATEADD(day, -90, GETDATE()) THEN 'Active (90d)'
WHEN LastLoginDate > DATEADD(day, -365, GETDATE()) THEN 'Inactive (1yr)'
ELSE 'Dormant'
END;
Map Data Fields
Map your legacy user fields to the Calimatic Auth schema:
| Legacy Field | Calimatic Auth Field | Notes |
|---|---|---|
Email | email | Required. Lowercased and trimmed. |
FirstName | firstName | Required. Max 100 characters. |
LastName | lastName | Required. Max 100 characters. |
Id | externalId | Store legacy ID for cross-referencing. |
TenantId | organizationId | Map legacy tenant to Calimatic organization UUID. |
RoleId | role | Map legacy role IDs to Calimatic role names. |
PasswordHash | passwordHash | Pass directly for seamless migration (bcrypt/PBKDF2). |
PhoneNumber | metadata.phone | Optional. Store in metadata. |
Map Legacy Roles
Create a mapping from your legacy role system to Calimatic roles:
const ROLE_MAP: Record<number, string> = {
1: "org_owner", // Legacy "Super Admin"
2: "org_admin", // Legacy "Admin"
3: "member", // Legacy "Staff"
4: "edtech_teacher", // Legacy "Teacher"
5: "edtech_student", // Legacy "Student"
6: "edtech_parent", // Legacy "Parent"
};
Map Legacy Tenants to Organizations
Ensure every legacy tenant has a corresponding Calimatic organization. Create organizations first if they do not exist:
curl -X POST https://auth.calimatic.com/api/v1/organizations \
-H "Content-Type: application/json" \
-H "x-api-key: your-api-key" \
-d '{
"name": "Springfield Elementary",
"slug": "springfield-elementary",
"domain": "springfield.edu",
"type": "school",
"plan": "professional"
}'
Build a mapping: legacyTenantId -> calimaticOrgId.
2. Set Up Dual Auth (Legacy + OIDC)
During the transition period, run both authentication systems in parallel. This prevents disruption for existing users.
Architecture During Transition
User Login
|
v
+-------------------+
| Login Page |
| (modified) |
+-------------------+
| |
v v
Legacy Auth OIDC Auth
(existing) (new)
| |
v v
Legacy DB Calimatic Auth
| |
v v
App Session (unified)
Implementation Strategy
-
Add OIDC as a secondary login option: Keep the existing login form. Add a "Sign in with Calimatic" button that triggers the OIDC flow.
-
Maintain session compatibility: Both login methods should produce the same session format so the rest of your app does not need changes.
-
Feature flag: Use a feature flag to control which login method is the default, allowing gradual rollout.
// Example: Dual auth in a Next.js app
if (process.env.AUTH_MODE === "oidc" || req.query.auth === "oidc") {
// New OIDC flow
redirect("/api/auth/signin");
} else {
// Legacy auth flow (existing code)
redirect("/login");
}
Pilot Group
Test with a small pilot group (5-10 users) before full migration:
- Create their accounts in Calimatic Auth via the Provisioning API
- Have them log in via the OIDC flow
- Verify they can access all app features
- Collect feedback and fix issues
3. Bulk Import Users
Use the Import API to migrate existing users into Calimatic Auth.
Prepare the Import Payload
Transform your legacy data into the import format:
import fs from "fs";
// Read your exported user data
const legacyUsers = JSON.parse(fs.readFileSync("legacy-users.json", "utf-8"));
const importPayload = {
users: legacyUsers.map((user: any) => ({
email: user.Email.toLowerCase().trim(),
firstName: user.FirstName,
lastName: user.LastName,
organizationId: tenantToOrgMap[user.TenantId], // Your mapping
role: ROLE_MAP[user.RoleId] || "member",
externalId: String(user.Id), // Preserve legacy ID
metadata: {
legacyTenantId: user.TenantId,
legacyRoleId: user.RoleId,
legacyCreatedDate: user.CreatedDate,
phone: user.PhoneNumber,
},
})),
defaultApplications: ["edtech"], // Auto-assign your app's license
sendInviteEmails: false, // Migration -- don't spam existing users
skipExisting: true, // Deduplicate by email
};
Execute the Import
The Import API is an alias for bulk provisioning with migration-friendly defaults (sendInviteEmails: false, skipExisting: true):
curl -X POST https://auth.calimatic.com/api/v1/users/import \
-H "Content-Type: application/json" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret" \
-d @import-payload.json
Response:
{
"success": true,
"data": {
"total": 250,
"created": 230,
"updated": 15,
"skipped": 0,
"failed": 5,
"message": "Import complete: 230 created, 15 updated, 5 failed",
"errors": [
{ "email": "bad-email", "error": "Must be a valid email address", "index": 42 }
],
"users": [
{ "email": "jane@school.edu", "userId": "uuid-...", "status": "user_created" }
]
}
}
Batch Large Imports
The API supports up to 500 users per request. For larger user bases, batch the import:
const BATCH_SIZE = 500;
const batches = [];
for (let i = 0; i < allUsers.length; i += BATCH_SIZE) {
batches.push(allUsers.slice(i, i + BATCH_SIZE));
}
for (const [index, batch] of batches.entries()) {
console.log(`Importing batch ${index + 1}/${batches.length} (${batch.length} users)...`);
const response = await fetch("https://auth.calimatic.com/api/v1/users/import", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": process.env.CALIMATIC_CLIENT_ID!,
"x-client-secret": process.env.CALIMATIC_CLIENT_SECRET!,
},
body: JSON.stringify({
users: batch,
defaultApplications: ["edtech"],
sendInviteEmails: false,
skipExisting: true,
}),
});
const result = await response.json();
console.log(` Created: ${result.data.created}, Failed: ${result.data.failed}`);
// Rate limit: wait between batches
await new Promise((resolve) => setTimeout(resolve, 2000));
}
4. Handle Password Migration
Existing users have passwords in your legacy system. There are four strategies:
Strategy A: Password Hash Migration (Recommended for Same Encryption)
If your legacy app uses bcrypt for password hashing (same as Calimatic Auth), you can migrate the hashes directly. Users log in immediately with their existing passwords -- no reset required.
Include the passwordHash field when importing users:
{
"users": [
{
"email": "jane@school.edu",
"firstName": "Jane",
"lastName": "Smith",
"passwordHash": "$2b$12$LJ3m4ys3Lk0TSwHiPbUXR.ZaWgmFLfD5hsOqpDumNcBEkb/FumBcG",
"externalId": "usr_12345"
}
],
"defaultOrganizationId": "org-uuid-here",
"sendInviteEmails": false
}
Supported formats: bcrypt ($2b$, $2a$) and PBKDF2 (Keycloak JSON format). PBKDF2 hashes are automatically upgraded to bcrypt on first login.
Pros: Zero friction for users. No email required. Immediate login with existing credentials.
Cons: Only works if the legacy app uses bcrypt or PBKDF2. Requires access to raw password hashes.
Strategy B: Password Reset Emails
After import, trigger password reset emails so users set a new password on the Calimatic Auth platform:
# For each imported user
curl -X POST https://auth.calimatic.com/api/v1/users/{userId}/reset-password \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret"
Pros: Clean break from legacy passwords. Users get a familiar "set your password" flow.
Cons: Requires user action. Users who do not reset their password cannot log in via OIDC.
Strategy C: Temporary Passwords
Set a temporary password during import. Users are forced to change it on first login:
{
"users": [
{
"email": "jane@school.edu",
"firstName": "Jane",
"lastName": "Smith",
"temporaryPassword": "TempP@ss2024!"
}
]
}
Pros: Users can log in immediately with the temp password.
Cons: Requires communicating the temp password to users securely.
Strategy D: SSO Bridge (No Password Needed)
If the organization uses Google Workspace or Microsoft 365, configure SSO for the organization. Users log in with their existing Google/Microsoft credentials -- no password migration needed.
5. Migrate from Legacy Exchange Flow to OIDC
If your app currently uses the legacy POST /api/v1/auth/exchange-code and POST /api/v1/auth/exchange flow, follow Section 11 of the Integration Guide for step-by-step migration instructions.
Key Differences
| Aspect | Legacy Flow | OIDC Flow |
|---|---|---|
| Token issuer | Keycloak | Calimatic platform |
| Token format | Keycloak JWT | Platform-signed RS256 JWT |
| Code exchange | Custom API | Standard OAuth 2.0 |
| PKCE | Not supported | Required (S256) |
| Consent | Not supported | Built-in consent screen |
| Discovery | N/A | /.well-known/openid-configuration |
Both flows can coexist during the transition. The legacy endpoints remain operational until you explicitly disable them.
6. Verify Migration
Check Migration Status
Use the migration status tool to monitor progress:
curl https://auth.calimatic.com/api/v1/tools/migration-status \
-H "Cookie: your-session-cookie"
Response:
{
"success": true,
"data": {
"totalUsers": 1500,
"migrationProgress": {
"migratedUsers": 1200,
"nativeUsers": 300,
"migrationPercentage": 80
},
"bySource": [
{ "source": "provisioning", "count": 1200 },
{ "source": "keycloak_sync", "count": 200 },
{ "source": "self_registration", "count": 100 }
],
"byStatus": [
{ "status": "active", "count": 1400 },
{ "status": "pending_verification", "count": 100 }
],
"health": {
"emailVerified": 1300,
"emailVerifiedPercentage": 87,
"hasLoggedIn": 1100,
"hasLoggedInPercentage": 73,
"hasOrganization": 1450,
"hasOrganizationPercentage": 97
}
}
}
Verification Checklist
- User count matches: Total imported users equals expected count from legacy database
- No duplicate accounts: Run the resolve endpoint for a sample of emails to confirm no duplicates
- Organization membership: Users are assigned to the correct organizations
- Roles are correct: Spot-check role assignments for a sample of users
- App licenses assigned: Users have licenses for your application
- Login works: Pilot users can log in via OIDC and access the app
- Legacy ID preserved:
externalIdis populated for cross-referencing
Resolve a User to Verify
curl "https://auth.calimatic.com/api/v1/users/resolve?email=jane@school.edu" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret"
Verify the response includes the correct organization, role, and license assignments.
7. Cutover Plan
Once migration is verified and the pilot group is stable, plan the cutover.
Pre-Cutover
- Announce the change: Notify users about the authentication change and timeline
- Send password reset emails: If using Strategy A, send reset emails at least 1 week before cutover
- Monitor login metrics: Track how many users have successfully logged in via OIDC
- Set a cutover date: Choose a low-traffic period (e.g., weekend or holiday)
Cutover Steps
- Switch the default login: Change
AUTH_MODEtooidc(or update your feature flag) - Update redirect URIs: Point your app's login to the OIDC authorize endpoint
- Disable legacy auth endpoints: Remove or disable the old login/exchange code endpoints
- Monitor error rates: Watch for authentication errors in the first 24 hours
Post-Cutover
- Send reminder emails: To users who have not yet logged in via the new system
- Keep legacy endpoints available (but hidden) for 30 days as a fallback
- Clean up: Remove legacy auth code, environment variables, and database tables after the grace period
8. Rollback Strategy
If critical issues arise during migration, roll back with minimal disruption.
During Dual Auth Phase (Before Cutover)
Simply revert to legacy auth as the default. No data loss -- users who logged in via OIDC can still use legacy auth.
After Cutover
- Re-enable legacy endpoints: Restore the legacy login flow
- Switch
AUTH_MODEback tolegacy: Or revert the feature flag - Investigate and fix the issue
- Re-attempt cutover once the issue is resolved
Data Rollback (Last Resort)
Users created only in Calimatic Auth (not in the legacy system) would lose access during a full rollback. To mitigate:
- Keep the legacy database read-only during migration (do not delete records)
- Maintain the
externalIdmapping so you can reconcile users - Export the list of newly created users before cutover
Related Documentation
- Integration Guide -- Section 11: Migrating from Legacy Exchange Flow
- User Management API
- User Provisioning Guide
- Webhooks API -- Set up webhooks to track user events during migration