When we talk about state management in Flutter, the conversation usually revolves around rebuilds, observability, immutability, dependency injection, side effects, events, and async state.

All of that matters.

But there is another question that, in my view, still doesn't sit at the center of the discussion nearly enough:

what happens to sensitive data after it enters state?

That question became impossible for me to ignore while working on Ekklesia Worship, a product I'm building to help churches create worship playbacks and lyric-based media. Even though the core of the product is media creation, once you introduce authentication, cloud sync, marketplace access, session restore, and account flows, the architecture starts dealing with much more than screens and UI updates.

It starts dealing with things like:

  • passwords
  • OTP codes
  • access tokens
  • refresh tokens
  • authenticated session restore
  • logout cleanup

And those values do not behave like ordinary UI state.

  • A password typed into a login screen should not simply stay in memory until someone remembers to overwrite it.
  • An OTP should not remain around after verification.
  • An access token should not be treated the same way as a page filter or a selected tab.
  • A refresh token should not just be another string stored somewhere because "the app needs it later."

That was the point where I started thinking beyond traditional state management. Not about replacing it. But about complementing it with something else:

a runtime that understands the lifecycle of sensitive data.

The problem starts when session becomes "just another object"

In many real apps, session-related logic gets distributed across multiple places.

A little in the ViewModel, a little in a service, a little in a repository, a little in dispose logic, a little in some manual clear() call someone added after a successful login… It works… until it doesn't.

The issue is not that the rules do not exist. The issue is that they are often implicit. And when important rules are implicit, they are easier to forget, easier to duplicate, and easier to implement inconsistently.

For example, a typical app may need to answer questions like:

  • Should the session survive app restarts?
  • Which part of the session can be persisted?
  • Should the access token be restored automatically?
  • Should the refresh token be restored only explicitly?
  • What should be cleared on logout?
  • What should disappear when the screen is disposed?
  • What should be masked in logs?
  • What should never be serialized?

These are not only state questions. These are retention, cleanup, expiration, persistence, and exposure questions. That is why I started thinking about this as more than state management.

This is not about "security magic"

Before going any further, I want to be very clear about what this idea is not. This is not about claiming perfect security.

It does not magically wipe memory at a cryptographic level.

It does not protect against debuggers.

It does not protect against compromised devices.

It does not solve memory dumps or Dart's internal string copies.

It does not eliminate every possible exposure vector.

That would be dishonest. What this tries to improve is something much more practical: safe handling.

That means:

  • reducing accidental retention
  • avoiding unintended persistence
  • making expiration explicit
  • making cleanup intentional
  • controlling exposure in logs and debug tools
  • separating UI state from sensitive runtime data more clearly

That alone is already very valuable in real applications.

State management answers one question well. Sensitive data needs another.

Over time, I found it useful to separate two concerns.

State management

Usually answers questions like:

  • what changed?
  • who is observing this?
  • when should the UI rebuild?
  • how do I represent loading, success, and error?

State runtime

Answers different questions:

  • how long should this value live?
  • when should it be cleared?
  • can it be persisted?
  • should it be restored automatically or explicitly?
  • should it be redacted in logs?
  • should it live only in memory?

In my view, those two things do not compete. They complement each other. Traditional state management is great at modeling UI behavior. But for sensitive data, there is often a second layer of concern that remains manual and scattered across the app.

That second layer is what I started calling State Runtime.

A real case: Ekklesia Worship

Ekklesia Worship is not an "auth app." It is a media creation product for churches.

Its core goal is to help users create worship playbacks, lyric-based visual content, and song media in a more organized and professional way.

But once features like cloud synchronization, account access, templates, and marketplace capabilities are introduced, authentication and session handling become part of the architecture too.

That made the problem concrete.

The product now includes flows like:

  • login
  • account creation
  • password recovery
  • OTP verification
  • authenticated session restore
  • logout

And suddenly the app is no longer dealing only with content state. It is also dealing with sensitive lifecycle. That was the practical context that pushed me to explore this idea more seriously.

SafeData: turning sensitive values into explicit contracts

The core thought was simple: What if a value like a password or OTP was not just a raw field inside state? What if it also carried an explicit lifecycle policy?

That led me to explore a primitive like this:

final password = SafeData<String?>(
  initialValue: typedPassword,
  policy: const SafeDataPolicy(
    clearOnDispose: true,
    clearOnCommandSuccess: {'login'},
    logStrategy: SafeDataLogStrategy.redacted,
  ),
);

final otpCode = SafeData<String?>(
  policy: const SafeDataPolicy(
    clearOnDispose: true,
    clearOnCommandSuccess: {'verifyOtp'},
    expiresAfter: Duration(seconds: 30),
    persistence: SafeDataPersistence.memoryOnly,
    logStrategy: SafeDataLogStrategy.masked,
  ),
);

The change looks small, but conceptually it is huge. The password is no longer just a String?. The OTP is no longer just a temporary field. They become values with retention rules.

This makes it possible to model sensitive data as something the architecture actively understands, not something the app only happens to store.

What this changes in practice

1. Passwords

A password usually has one of the shortest useful lifecycles in the entire app. It needs to exist:

  • while the user types it
  • while the login request is in progress

After a successful login, keeping that value around usually makes no sense. With an explicit policy, the runtime can clear that password automatically after the login command succeeds.

That removes one more "don't forget to clean this up later" rule from scattered application code.

2. OTP codes

OTP codes are an even clearer example of ephemeral data. They often need:

  • short expiration
  • automatic cleanup after success
  • cleanup on screen disposal
  • masked or redacted logging

Without explicit lifecycle handling, OTP values tend to remain available simply because nobody removed them.

By giving them a runtime policy, their behavior becomes part of the architecture instead of a convention.

3. Access tokens

Access tokens often make sense only as short-lived in-memory session values.

  • They should not necessarily be serialized by default.
  • They should not necessarily remain attached to a ViewModel beyond the active session.
  • They should not show up raw in logs or debug tooling.

This is not just a state question. It is a runtime retention question.

4. Refresh tokens

Refresh tokens usually require a different lifecycle altogether.

Unlike passwords or OTPs, they may need:

  • secure persistence
  • controlled restoration
  • explicit deletion on logout
  • redacted logging

And that distinction matters. Because not all sensitive values should be treated the same way. That is one of the strongest reasons why "state" alone often feels too broad as a model.

Session persistence is where this becomes really useful

The part of this idea that felt most valuable to me was not password cleanup by itself.

It was session persistence. That is where the runtime mindset really starts to show its worth. A more explicit session model might look like this:

  • password: temporary, clear after login
  • OTP: temporary, expiring, clear after verification
  • access token: memory only
  • refresh token: optionally persisted in secure storage
  • session metadata: regular state
  • restore: explicit and controlled
  • logout: centralized cleanup of everything

For example:

final accessToken = SafeData<String?>(
  policy: const SafeDataPolicy(
    clearOnDispose: true,
    persistence: SafeDataPersistence.memoryOnly,
    logStrategy: SafeDataLogStrategy.redacted,
  ),
);

final refreshToken = SafeData<String?>(
  policy: const SafeDataPolicy(
    clearOnDispose: true,
    persistence: SafeDataPersistence.secureStorage,
    logStrategy: SafeDataLogStrategy.redacted,
  ),
);

At that point, the session is no longer just an object you happen to store somewhere. It becomes a composition of values with different rules, and that is exactly the kind of nuance that generic state containers usually do not model by themselves.

Explicit secure persistence should be optional, not accidental

One important design choice that came out of this thinking was separation. I do not think secure persistence should be silently bundled into the core of a state package as a default side effect, instead, I think it makes more sense to keep that capability explicit and optional. That is why the idea naturally expanded into something like a separate package:

  • flutter_stasis for runtime-aware state management
  • flutter_stasis_secure for optional secure persistence bindings

Something along these lines:

final binding = SafeDataSecureBinding<String>(
  field: refreshToken,
  adapter: adapter,
  key: 'refresh_token',
  encode: (value) => value,
  decode: (value) => value,
);

Again, the important part is not the exact API. It is the principle:

sensitive persistence should happen because the architecture chose it explicitly, not because the whole state object was serialized by accident.

A real flow: login >> OTP >> authenticated session >> restore >> logout

The most useful way to look at this is as a full flow.

Step 1 — Login

The user types email and password. The password enters the runtime as a sensitive value. It exists only while needed, is redacted in logs, and is cleared after a successful login command.

Step 2 — OTP verification

If the account requires two-factor verification, the OTP enters as another sensitive runtime value. It can expire after a short time and clear itself automatically after success.

Step 3 — Authenticated session

The app now receives session data. The access token can remain memory-only. The refresh token may be bound to secure storage if the app wants persistent session restore.

Step 4 — Session restore

When the app starts again, session restoration does not need to be some vague side effect. It can be explicit, controlled, and testable.

Step 5 — Logout

Logout becomes a single architectural cleanup point. It can clear:

  • password remnants
  • OTP values
  • access token
  • refresh token in memory
  • refresh token in secure persistence

That kind of explicitness makes the whole auth flow easier to reason about and harder to accidentally mishandle.

But don't Riverpod, Bloc, and others already solve this?

They solve important parts of the problem very well.

Riverpod, Bloc, Provider, MobX, Signals, and other approaches are all valuable depending on what you need.

They help a lot with things like:

  • reactivity
  • state composition
  • dependency injection
  • side effects
  • predictable UI updates
  • architecture boundaries

The point here is not that they are wrong or insufficient as state managers. The point is narrower:

in most projects, the lifecycle of sensitive data still ends up being handled manually by the application. That means even with an excellent state manager, you still need to decide:

  • when to clear a password
  • when OTP expires
  • how to avoid unintended persistence
  • how to avoid raw exposure in logs
  • how to restore tokens in a controlled way
  • how to centralize cleanup on logout

That is the gap this idea is trying to explore. Not "better reactivity". Not "better state management". But a more explicit model for sensitive data lifecycle.

A light comparison, not a benchmark

If I were to compare this conceptually, I would frame it like this:

  • most state managers answer "what changed?"
  • what I am trying to explore is also "how long should this sensitive value live?"

That is not an attack on existing tools. It is a different architectural concern.

And in real products, especially anything involving auth, session restore, account recovery, or tokens, that concern keeps showing up whether we model it explicitly or not.

So the question is not whether apps need to handle it. They already do. The real question is whether we want that logic to stay implicit and scattered, or whether we want to make it a first-class part of the runtime.

What this improves

I think the practical value of this approach is easy to miss if you focus only on the abstraction name.

The real improvements are things like:

  • less accidental retention
  • less unintended persistence
  • clearer cleanup rules
  • more explicit expiration behavior
  • safer debug output
  • more centralized logout cleanup
  • better testability of lifecycle decisions
  • less auth-sensitive logic scattered across multiple layers

In other words, the gain is not "perfect security." The gain is less architectural ambiguity. And that matters a lot.

What this does not solve

It is just as important to be clear about what this approach does not solve.

It does not guarantee:

  • resistance against debuggers
  • safety on compromised devices
  • protection against memory dumps
  • cryptographic zeroization of strings
  • absolute secrecy inside the runtime

Those are different layers of the problem. What this approach tries to improve is:

  • lifecycle clarity
  • retention discipline
  • cleanup discipline
  • controlled persistence
  • reduced accidental exposure

That is already a meaningful step forward without pretending to solve more than it does.

Why this matters to me

What made this worth exploring for me is that it turned an "implementation detail" into an architectural question. Before this, cleanup rules for sensitive data often lived in comments, conventions, and "we should remember to…"

After this line of thinking, those rules started to feel like something that deserved to be modeled explicitly. That is what I find interesting about the idea of a State Runtime. It is not about replacing state management.

It is about recognizing that some data — especially passwords, OTPs, tokens, and session artifacts — carry lifecycle requirements that deserve first-class treatment.

Conclusion

State management solves a lot. But for sensitive data, it does not always answer the most important question: how should this value live?

Passwords, OTPs, access tokens, refresh tokens, and session restore are not just "more state."

They have retention rules.

They have cleanup rules.

They have persistence constraints.

They have exposure risks.

That is why I started exploring ideas like SafeData, optional secure bindings, and the broader notion of State Runtime in the Stasis ecosystem. Maybe the real question is not whether a state manager should "do security."

Maybe the more useful question is this: should a state architecture completely ignore the lifecycle of sensitive data?

I do not think it should.

Link for Packages: https://pub.dev/packages/flutter_stasis_secure https://pub.dev/packages/flutter_stasis https://github.com/DIMAAGR/flutter_stasis