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:manage permission
  • 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 FieldCalimatic Auth FieldNotes
EmailemailRequired. Lowercased and trimmed.
FirstNamefirstNameRequired. Max 100 characters.
LastNamelastNameRequired. Max 100 characters.
IdexternalIdStore legacy ID for cross-referencing.
TenantIdorganizationIdMap legacy tenant to Calimatic organization UUID.
RoleIdroleMap legacy role IDs to Calimatic role names.
PasswordHashpasswordHashPass directly for seamless migration (bcrypt/PBKDF2).
PhoneNumbermetadata.phoneOptional. 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

  1. Add OIDC as a secondary login option: Keep the existing login form. Add a "Sign in with Calimatic" button that triggers the OIDC flow.

  2. Maintain session compatibility: Both login methods should produce the same session format so the rest of your app does not need changes.

  3. 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:

  1. Create their accounts in Calimatic Auth via the Provisioning API
  2. Have them log in via the OIDC flow
  3. Verify they can access all app features
  4. 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

AspectLegacy FlowOIDC Flow
Token issuerKeycloakCalimatic platform
Token formatKeycloak JWTPlatform-signed RS256 JWT
Code exchangeCustom APIStandard OAuth 2.0
PKCENot supportedRequired (S256)
ConsentNot supportedBuilt-in consent screen
DiscoveryN/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: externalId is 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

  1. Announce the change: Notify users about the authentication change and timeline
  2. Send password reset emails: If using Strategy A, send reset emails at least 1 week before cutover
  3. Monitor login metrics: Track how many users have successfully logged in via OIDC
  4. Set a cutover date: Choose a low-traffic period (e.g., weekend or holiday)

Cutover Steps

  1. Switch the default login: Change AUTH_MODE to oidc (or update your feature flag)
  2. Update redirect URIs: Point your app's login to the OIDC authorize endpoint
  3. Disable legacy auth endpoints: Remove or disable the old login/exchange code endpoints
  4. Monitor error rates: Watch for authentication errors in the first 24 hours

Post-Cutover

  1. Send reminder emails: To users who have not yet logged in via the new system
  2. Keep legacy endpoints available (but hidden) for 30 days as a fallback
  3. 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

  1. Re-enable legacy endpoints: Restore the legacy login flow
  2. Switch AUTH_MODE back to legacy: Or revert the feature flag
  3. Investigate and fix the issue
  4. 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 externalId mapping so you can reconcile users
  • Export the list of newly created users before cutover

Related Documentation