Skip to content

Add the SuperTokens lazy user migration flow#124

Merged
mhamann merged 2 commits into
rownd:mainfrom
bcbogdan:feat/supertokens-migrate
Apr 24, 2026
Merged

Add the SuperTokens lazy user migration flow#124
mhamann merged 2 commits into
rownd:mainfrom
bcbogdan:feat/supertokens-migrate

Conversation

@bcbogdan
Copy link
Copy Markdown
Contributor

@bcbogdan bcbogdan commented Apr 21, 2026

Summary

Adds SuperTokens lazy user migration support to the iOS SDK.
When the SuperTokens config is included and a Rownd sign-in completes for a new_user, the SDK now calls the the ${apiBasePath}/plugin/rownd/migrate endpoint.

  • adds SuperTokensAppInfo and SuperTokensConfig to RowndConfig
  • registers a SuperTokens sync event handler during Rownd.configure(...)
  • listens for signInCompleted and only triggers migration for new_user
  • sends the Rownd access token to ${apiBasePath}/plugin/rownd/migrate

The main SDK behavior lives in:

  • Sources/Rownd/Models/RowndConfig.swift
  • Sources/Rownd/Rownd.swift
  • Sources/Rownd/framework/SuperTokensSync.swift

Summary by Sourcery

Add support for lazily migrating newly created Rownd users to SuperTokens in the iOS SDK.

New Features:

  • Introduce SuperTokens configuration types on RowndConfig to enable SuperTokens integration.
  • Automatically register a SuperTokens sync event handler during SDK configuration to observe authentication events.
  • Trigger a SuperTokens migration request for newly signed-in users using the Rownd access token.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 21, 2026

Reviewer's Guide

Adds SuperTokens lazy user migration support by extending Rownd configuration with SuperTokens metadata, registering a SuperTokens-specific Rownd event handler during configuration, and performing a one-time migration call to the SuperTokens migrate endpoint for newly created users after sign-in completes.

Sequence diagram for SuperTokens lazy user migration on sign-in

sequenceDiagram
    actor User
    participant iOSApp
    participant RowndSDK
    participant ContextEventBus
    participant SuperTokensSyncEventHandler
    participant RowndAuth
    participant SuperTokensAPI

    User->>iOSApp: Initiate_sign_in
    iOSApp->>RowndSDK: Rownd.signIn
    RowndSDK-->>RowndSDK: Authenticate_user
    RowndSDK-->>ContextEventBus: Emit_RowndEvent(signInCompleted,\nuser_type=new_user)

    ContextEventBus-->>SuperTokensSyncEventHandler: Dispatch_RowndEvent

    SuperTokensSyncEventHandler-->>SuperTokensSyncEventHandler: Validate_event
    Note over SuperTokensSyncEventHandler: Checks event_type == signInCompleted\nand user_type == new_user and Rownd.config.supertokens.appInfo != nil

    SuperTokensSyncEventHandler->>RowndAuth: Rownd.getAccessToken()
    RowndAuth-->>SuperTokensSyncEventHandler: accessToken

    SuperTokensSyncEventHandler->>SuperTokensAPI: POST /plugin/rownd/migrate\nAuthorization: Bearer_accessToken\nBase: apiDomain + apiBasePath
    SuperTokensAPI-->>SuperTokensSyncEventHandler: 2xx_on_success_or_error_status

    SuperTokensSyncEventHandler-->>SuperTokensSyncEventHandler: Log_result_and_return
Loading

Updated class diagram for Rownd SuperTokens configuration and sync

classDiagram
    class SuperTokensAppInfo {
        +String appName
        +String apiDomain
        +String apiBasePath
        +init(appName: String, apiDomain: String, apiBasePath: String)
    }

    class SuperTokensConfig {
        +SuperTokensAppInfo appInfo
        +init(appInfo: SuperTokensAppInfo)
    }

    class RowndConfig {
        +SuperTokensConfig supertokens
        +String appKey
        +String appGroupPrefix
        +Bool enableSmartLinkPasteBehavior
        +String signInLinkPattern
    }

    class Rownd {
        +static RowndConfig config
        +static func configure(config: RowndConfig)
        +static func getAccessToken() async throws String
    }

    class RowndEvent {
        +RowndEventType event
        +Dictionary data
    }

    class RowndEventHandlerDelegate {
        <<protocol>>
        +func handleRowndEvent(event: RowndEvent)
    }

    class Context {
        +static Context currentContext
        +Array eventListeners
    }

    class SuperTokensSyncEventHandler {
        -OSLog log
        +func handleRowndEvent(event: RowndEvent)
    }

    class SuperTokensSyncModule {
        +static func registerSuperTokensSyncEventHandler()
        +static func syncUserToSuperTokens(accessToken: String, appInfo: SuperTokensAppInfo) async
    }

    RowndConfig --> SuperTokensConfig : has_optional
    SuperTokensConfig --> SuperTokensAppInfo : has

    SuperTokensSyncEventHandler ..|> RowndEventHandlerDelegate : implements
    SuperTokensSyncEventHandler --> SuperTokensAppInfo : reads
    SuperTokensSyncEventHandler --> Rownd : uses
    SuperTokensSyncEventHandler --> RowndEvent : handles

    SuperTokensSyncModule --> SuperTokensSyncEventHandler : manages_singleton
    SuperTokensSyncModule --> Context : registers_listener
    SuperTokensSyncModule --> SuperTokensAppInfo : builds_endpoint
    SuperTokensSyncModule --> Rownd : uses_access_token

    Rownd --> RowndConfig : uses_config
Loading

File-Level Changes

Change Details Files
Introduce SuperTokens configuration types and integrate them into RowndConfig so the SDK can be configured with SuperTokens app info.
  • Add SuperTokensAppInfo struct with app name, API domain, and optional API base path defaulting to /auth
  • Add SuperTokensConfig struct wrapping SuperTokensAppInfo
  • Extend RowndConfig with an optional supertokens property to hold SuperTokensConfig
Sources/Rownd/Models/RowndConfig.swift
Register a SuperTokens-specific Rownd event handler during SDK configuration so migration can run after sign-in completes.
  • Invoke registerSuperTokensSyncEventHandler() during Rownd.configure before store cache inflation
Sources/Rownd/Rownd.swift
Implement SuperTokens lazy migration handler that listens for sign-in completion of new users and calls the SuperTokens migrate endpoint with the Rownd access token.
  • Create SuperTokensSyncEventHandler implementing RowndEventHandlerDelegate that filters for signInCompleted events with user_type new_user and presence of SuperTokens appInfo
  • Register a singleton SuperTokensSyncEventHandler instance with the global Rownd event listener list only when SuperTokens config is present and not already registered
  • Implement syncUserToSuperTokens to POST to {apiDomain}{apiBasePath}/plugin/rownd/migrate with Authorization: Bearer , handling non-2xx responses and errors via OSLog
Sources/Rownd/framework/SuperTokensSync.swift

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add SuperTokens lazy user migration flow

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds SuperTokens lazy user migration support to iOS SDK
• Implements automatic user sync on new sign-in completion
• Sends Rownd access token to SuperTokens migration endpoint
• Registers event handler during SDK configuration
Diagram
flowchart LR
  A["Rownd.configure"] --> B["registerSuperTokensSyncEventHandler"]
  B --> C["SuperTokensSyncEventHandler registered"]
  D["signInCompleted event"] --> E["Check if new_user"]
  E --> F["Get Rownd accessToken"]
  F --> G["POST to SuperTokens migrate endpoint"]
  G --> H["User synced to SuperTokens"]
Loading

Grey Divider

File Changes

1. Sources/Rownd/Models/RowndConfig.swift ✨ Enhancement +22/-0

Add SuperTokens configuration models

• Adds SuperTokensAppInfo struct with appName, apiDomain, and apiBasePath fields
• Adds SuperTokensConfig struct to wrap SuperTokensAppInfo
• Adds optional supertokens property to RowndConfig for SuperTokens configuration

Sources/Rownd/Models/RowndConfig.swift


2. Sources/Rownd/Rownd.swift ✨ Enhancement +2/-0

Register SuperTokens sync handler on configure

• Calls registerSuperTokensSyncEventHandler() during SDK configuration
• Ensures SuperTokens event listener is registered before state inflation

Sources/Rownd/Rownd.swift


3. Sources/Rownd/framework/SuperTokensSync.swift ✨ Enhancement +66/-0

Implement SuperTokens user migration sync handler

• Implements SuperTokensSyncEventHandler to listen for sign-in completion events
• Filters for new_user events and validates SuperTokens configuration
• Retrieves Rownd access token and sends POST request to SuperTokens migrate endpoint
• Includes error handling and logging for migration failures
• Prevents duplicate event handler registration

Sources/Rownd/framework/SuperTokensSync.swift


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 21, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Forced unwrap migrate URL🐞 Bug ☼ Reliability
Description
syncUserToSuperTokens force-unwraps URL(string:) for the migrate endpoint, so a malformed
apiDomain/apiBasePath will crash the app when a new user completes sign-in. This is
user-configurable input and is currently not validated or normalized before URL construction.
Code

Sources/Rownd/framework/SuperTokensSync.swift[54]

+        var request = URLRequest(url: URL(string: "\(base)/plugin/rownd/migrate")!)
Evidence
The migrate URL is built by string concatenation from SuperTokensAppInfo fields and then
force-unwrapped; SuperTokensAppInfo accepts arbitrary strings, so invalid/missing scheme, illegal
characters, or bad slash-joining can make URL(string:) return nil and trap at runtime.

Sources/Rownd/framework/SuperTokensSync.swift[47-56]
Sources/Rownd/Models/RowndConfig.swift[11-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`syncUserToSuperTokens` uses `URL(string: ...)!` for a URL derived from `SuperTokensAppInfo.apiDomain` and `.apiBasePath`. If the config produces an invalid URL (e.g., missing scheme like `api.example.com`, whitespace, or bad slash joining), the app will crash when the migration triggers.
### Issue Context
This is invoked asynchronously on `.signInCompleted` for `new_user`. Configuration values come from app integrators and should be treated as untrusted/misconfigurable.
### Fix Focus Areas
- Sources/Rownd/framework/SuperTokensSync.swift[47-66]
- Sources/Rownd/Models/RowndConfig.swift[11-20]
### Implementation notes
- Replace the force unwrap with `guard let url = ... else { log.error(...); return }`.
- Build the endpoint using `URLComponents` or `URL` path-appending APIs; normalize leading/trailing slashes between `apiDomain` and `apiBasePath`.
- Consider enforcing `https` (or at least logging/rejecting non-https) and setting an explicit `request.timeoutInterval` to avoid long hangs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Racy eventListeners mutation 🐞 Bug ☼ Reliability
Description
registerSuperTokensSyncEventHandler mutates Context.currentContext.eventListeners without any
synchronization, which can race with RowndEventEmitter iterating listeners on the MainActor and
can also double-register under concurrent calls. This can lead to duplicate migrations or a crash
due to concurrent mutation during iteration.
Code

Sources/Rownd/framework/SuperTokensSync.swift[R38-44]

+    let alreadyRegistered = Context.currentContext.eventListeners.contains { listener in
+        listener === superTokensSyncEventHandler
+    }
+
+    if !alreadyRegistered {
+        Context.currentContext.eventListeners.append(superTokensSyncEventHandler)
+    }
Evidence
eventListeners is a plain mutable array; RowndEventEmitter iterates it on the MainActor, while
Rownd.configure is not @MainActor and calls the registration function before any actor hop. The
contains-then-append is also a non-atomic check-then-act sequence, so concurrent callers can both
append.

Sources/Rownd/Models/Context/Context.swift[11-18]
Sources/Rownd/framework/RowndEvent.swift[33-64]
Sources/Rownd/framework/SuperTokensSync.swift[33-45]
Sources/Rownd/Rownd.swift[43-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Context.currentContext.eventListeners` is mutated from `registerSuperTokensSyncEventHandler()` without thread-safety guarantees. Since events are emitted on `MainActor` and `configure()` can run off-main, this creates a reader/writer race and a non-atomic dedupe that can double-register the handler.
### Issue Context
`RowndEventEmitter` is `@MainActor` and iterates `eventListeners`. Any off-main append during emission can cause unpredictable behavior.
### Fix Focus Areas
- Sources/Rownd/framework/SuperTokensSync.swift[33-45]
- Sources/Rownd/framework/RowndEvent.swift[33-64]
- Sources/Rownd/Rownd.swift[43-58]
- Sources/Rownd/Models/Context/Context.swift[11-18]
### Implementation notes
Choose one:
- Mark `registerSuperTokensSyncEventHandler()` as `@MainActor` and call it via `await MainActor.run { ... }` in `Rownd.configure`, ensuring all mutations happen on main.
- Or protect `eventListeners` behind a lock/serial queue, and ensure `notifyListeners` and append operations use the same synchronization.
- Also consider centralizing dedupe logic inside `Rownd.addEventHandler` to avoid multiple call sites with inconsistent behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Extension early-return bypass 🐞 Bug ≡ Correctness
Description
Rownd.configure now registers the SuperTokens event handler before the app-extension early return,
introducing new side effects in app extensions that previously skipped most setup. This can lead to
extension contexts attaching listeners and potentially performing network migration work later.
Code

Sources/Rownd/Rownd.swift[R51-55]

+        registerSuperTokensSyncEventHandler()
+        
     let state = await inst.inflateStoreCache()
     // Skip the rest within app extensions
Evidence
The app-extension early return remains in configure, but the new SuperTokens handler registration
occurs before it. The registered handler can later call syncUserToSuperTokens, which uses
URLSession to make a network request.

Sources/Rownd/Rownd.swift[43-58]
Sources/Rownd/framework/SuperTokensSync.swift[33-45]
Sources/Rownd/framework/SuperTokensSync.swift[47-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Rownd.configure` registers the SuperTokens sync event handler before checking for app extensions (`.appex`) and returning early. This changes extension behavior by performing extra registration work and enabling later network migration behavior in extension contexts.
### Issue Context
The existing `configure` method explicitly skips “the rest” for app extensions; SuperTokens registration should likely follow that same rule.
### Fix Focus Areas
- Sources/Rownd/Rownd.swift[43-58]
### Implementation notes
- Move `registerSuperTokensSyncEventHandler()` to after the `.appex` early return.
- Alternatively (or additionally), add an explicit app-extension guard inside `registerSuperTokensSyncEventHandler()`.
- If the desired behavior is to support SuperTokens migration in extensions, document it and ensure it’s safe for extension execution constraints.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In syncUserToSuperTokens, avoid force-unwrapping the URL (i.e., URL(string: ...)!) and instead gracefully handle an invalid apiDomain/apiBasePath by safely constructing the URL and logging an error if it fails.
  • Given RowndConfig is Encodable and supertokens is added under the "These will not be encoded" section, double-check that the coding keys or custom encoding logic actually exclude supertokens the same way as the other non-encoded properties.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `syncUserToSuperTokens`, avoid force-unwrapping the URL (i.e., `URL(string: ...)!`) and instead gracefully handle an invalid `apiDomain`/`apiBasePath` by safely constructing the URL and logging an error if it fails.
- Given `RowndConfig` is `Encodable` and `supertokens` is added under the "These will not be encoded" section, double-check that the coding keys or custom encoding logic actually exclude `supertokens` the same way as the other non-encoded properties.

## Individual Comments

### Comment 1
<location path="Sources/Rownd/framework/SuperTokensSync.swift" line_range="54" />
<code_context>
+    let base = "\(appInfo.apiDomain)\(appInfo.apiBasePath)"
+
+    do {
+        var request = URLRequest(url: URL(string: "\(base)/plugin/rownd/migrate")!)
+        request.httpMethod = "POST"
+        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
</code_context>
<issue_to_address>
**issue (bug_risk):** Force-unwrapping the SuperTokens migration URL can crash on misconfiguration.

Because this URL is built from external config, `URL(string: ...)!` can crash the app if `apiDomain`/`apiBasePath` are malformed (missing scheme, extra slashes, spaces, etc.). Consider safely handling failures:

```swift
let base = "\(appInfo.apiDomain)\(appInfo.apiBasePath)"

guard let url = URL(string: "\(base)/plugin/rownd/migrate") else {
    log.error("[Rownd->ST] invalid migration URL constructed from base: \(base)")
    return
}

var request = URLRequest(url: url)
```

This prevents a crash while still exposing the misconfiguration via logs.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread Sources/Rownd/framework/SuperTokensSync.swift Outdated
Comment thread Sources/Rownd/framework/SuperTokensSync.swift Outdated
Comment thread Sources/Rownd/Rownd.swift
@bcbogdan bcbogdan force-pushed the feat/supertokens-migrate branch from 9409768 to f560aad Compare April 22, 2026 18:13
@mhamann mhamann merged commit 221d6bc into rownd:main Apr 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants