← All posts
Engineering · · 6 min read

Migrating anonymous feedback to real accounts

Making people sign in before they can complain is how you get no feedback. The SDK starts everyone anonymous and migrates them once, in one direction.

If a user has to create an account before they can report a bug, most of them do not report the bug. So the fedo SDK gives every user an identity whether or not your app knows who they are: open the feedback screen without calling setUserID and the SDK mints an anonymous user with a random ID, caches it, and lets them post, vote, and comment immediately. When they later sign in, everything they created moves to the real account and the anonymous one is deleted.

That migration shipped in SDK 0.2.1 on 23 July 2026. This post is about why it works the way it does, and what the one-way constraint costs.

Key takeaways

  • Anonymous users are created lazily, on first open of the feedback screen — not at initialize.
  • The anonymous ID is cached on device, so the same person keeps the same identity across app sessions.
  • setUserID migrates the anonymous user's feedback, comments, and votes to the authenticated user, then permanently deletes the anonymous record.
  • Migration runs in one direction only: anonymous to authenticated, once. There is no path back.
  • logout() does not restore the old anonymous user. It clears the cache and creates a brand new one.

Why not just require an account?

Because the cost lands entirely on the person doing you a favour.

A user who hits a bug is already annoyed. Ask them to create an account at that moment and you have inserted a signup form between the annoyance and the report. Some fraction pushes through. The rest close the app, and you never learn the thing they were about to tell you.

The other reason is that a lot of apps genuinely do not have accounts. A calculator, a habit tracker, a local-first notes app — asking them to build authentication so their users can file feature requests is absurd.

The design constraint: feedback has to work for a user your app cannot name. Everything else follows from that.

Why lazily, and why cached?

The anonymous user is created when the feedback screen is first opened, not when the SDK initializes. Most users of most apps never open a feedback board. Creating a record for every install would mean the overwhelming majority of user rows in the system are people who never said anything — noise in every count, every dashboard, every query.

Caching the ID matters more than it looks. Without it, a user who reports a bug on Monday and comes back on Wednesday is a different person: they cannot find their own report, cannot see the reply, and their Mine tab is empty. With the ID cached on device, an anonymous user is durable — they get a thread, they get continuity, they can watch the status change. It is a real identity that just happens to have no name attached.

The tradeoff is that identity is bound to the device and the app install. Clear the app data and the anonymous user is gone with it. That is the correct behaviour for something that carries no credential — the alternative is a device fingerprint, which is exactly the passive tracking fedo does not do.

The migration

AuthRepository.kt
// user has been posting anonymously for two weeks
Fedo.setUserID("user-abc-123")
// their feedback, comments, and votes now belong to user-abc-123

One call. The SDK moves the anonymous user's feedback items, comments, and votes onto the newly authenticated user, then permanently deletes the anonymous record.

Deleting rather than keeping it around is deliberate. A merged-away anonymous user that still exists is a second identity for one person: it can still hold a vote, still show as a comment author, still be counted. Vote counts are the thing that decides what gets built next, so an identity that can double-count is worse than useless.

Migration is only anonymous → authenticated. There is no reverse path and no merge between two authenticated users. If someone posts under user-a, logs out, and signs in as user-b, those stay two separate people — which is correct, because as far as the system can tell, they are.

Why logout does not restore the previous anonymous user

logout() clears everything cached — token, ID, display name, email, properties — and immediately creates a fresh anonymous user. It does not restore the anonymous identity that existed before login, and on an already-anonymous user it does nothing at all.

This surprises people, so it is worth stating plainly: after a logout, the previous anonymous identity is not sitting there waiting. It was consumed by the migration. What you get is a new anonymous user with a new random ID and an empty history.

The alternative — keeping the pre-login anonymous user parked in case of logout — means holding a duplicate identity indefinitely for every user who ever signed in, on the chance they sign out again. For the same double-counting reason as above, that is not worth it.

The queue, and what it does not do

All the identity methods can be called synchronously, in any order, from anywhere. An internal queue serializes them so they execute one at a time in the right order — you do not have to await setUserID before calling setUserEmail, and you cannot race them into a bad state.

What the queue does not do is retry:

A failing operation is skipped and the queue continues with the next one. There is no retry and no offline support — if a call fails, you must call it again. Offline support is planned for a future version.

Worth being clear-eyed about what that means in practice. A user on a flaky connection who signs in at exactly the wrong moment can have setUserID fail, which means the migration does not happen, which means their anonymous feedback stays anonymous. Calling setUserID again on the next app start fixes it — and since calling it with the same ID is a no-op once it has succeeded, calling it on every launch is a safe pattern rather than a wasteful one.

That is the workaround I would actually recommend today: call setUserID on every app start where you know the user, not only in the login handler.

What I would change

The migration model is right and I would build it the same way again. The gap is the retry story. "Call it again on next launch" is a workaround that works, but it puts the burden on every integrator to know about it, and the failure it protects against is invisible — nothing tells the developer that a migration silently did not happen.

Offline support is the planned fix, and a durable queue that survives process death is the shape it should take. Until then, the honest summary is: the identity model is solid, the delivery guarantees are not, and calling setUserID idempotently on every launch covers most of the distance.

The full behaviour is documented on the user management page, and the release it shipped in is in the Android changelog.

AndroidSDKIdentityProduct DesignData Migration

Ship feedback that listens.

Start free with 1 board, upgrade whenever you need more.

See pricing