DEV Community

Cover image for State Management + Security: Why Sensitive Data Needs a Runtime, Not Just State
Italo Matos
Italo Matos

Posted on

State Management + Security: Why Sensitive Data Needs a Runtime, Not Just State

While building a real Flutter app, I kept running into a question that I don’t see discussed very often:

what happens to sensitive data after it enters state?

Most state management discussions focus on UI concerns:

  • rebuilds
  • observability
  • async flows
  • dependency composition
  • side effects

All of that matters.

But in real apps, we also deal with values like:

  • passwords
  • OTP codes
  • access tokens
  • refresh tokens
  • session restore data

And these values are not just “more state”.

They have retention rules.

They have cleanup rules.

They have persistence constraints.

They have exposure risks.

That was the problem that pushed me to think about something beyond regular state management.

Not replacing it.

Just adding another layer of concern to the architecture.

The real-world context

This came up while I was building Ekklesia Worship, a Flutter app for creating worship playbacks for churches.

The product itself is media-focused, but once you add things like:

  • login
  • account creation
  • OTP verification
  • session restore
  • logout
  • marketplace access

auth and session data become part of the architecture too.

And that is where the problem starts to feel very real.

A password typed during login should not just sit in memory until something happens to overwrite it.

An OTP should not remain around after verification.

A refresh token should not be treated like just another string in a ViewModel.

State management solves one part of the problem

Most state managers do a good job answering questions like:

  • what changed?
  • who should react to it?
  • when should the UI rebuild?
  • how do I represent loading, success, and error?

That part is already well explored.

The part that kept bothering me was different:

  • how long should this sensitive value live?
  • when should it be cleared?
  • should it expire automatically?
  • should it be persisted at all?
  • should it show up redacted in logs?
  • should it exist only in memory?

That started to feel like a runtime concern, not just a UI state concern.

The idea: explicit lifecycle for sensitive data

The idea I started exploring was simple:

what if sensitive values were not just raw fields inside state, but values with explicit lifecycle policies?

Something 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,
  ),
);
Enter fullscreen mode Exit fullscreen mode

The important part here is not the exact API.

The important part is the shift in modeling.

The password stops being just a String?.

The OTP stops being just another field in state.

Both start carrying explicit lifecycle rules.

Why this matters

In practice, this changes a lot.

Passwords

Passwords usually have a very short useful lifetime.

They need to exist:

  • while the user types
  • while the login request is running

After that, keeping them around usually does not make sense.

OTP codes

OTP values are even more obviously ephemeral.

They may need:

  • short expiration
  • automatic cleanup after success
  • cleanup on dispose
  • masked logging

Access tokens

Access tokens often make sense only in memory and only for a short time.

Refresh tokens

Refresh tokens usually have a different lifecycle:

  • secure persistence
  • explicit restore
  • cleanup on logout
  • no raw exposure in logs

That distinction is exactly why “state” alone often feels too broad.

Session persistence is where this gets interesting

The part that made this idea feel really useful was session persistence.

Because once you model a session more carefully, you start realizing that not every sensitive value should be treated the same way.

A more explicit model might look like this:

  • password: temporary
  • OTP: temporary and expiring
  • access token: memory-only
  • refresh token: optionally persisted securely
  • restore: explicit
  • logout: centralized cleanup

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,
  ),
);
Enter fullscreen mode Exit fullscreen mode

That makes session handling feel much more intentional.

Important nuance

I do not think every token should live in UI state.

In many cases, the better path is to let the auth layer own session persistence and read the current session on demand.

That is usually cleaner.

The problem I’m pointing at is slightly broader:

even if tokens themselves are not stored in app state, sensitive values still pass through the app runtime during real flows:

  • passwords
  • OTP codes
  • recovery data
  • temporary credentials
  • transition/merge cases
  • restore artifacts

So the question becomes less:

should tokens live in state?

And more:

how explicitly are we handling the lifecycle of sensitive values when they inevitably exist somewhere in the app runtime?

This is not “perfect security”

I think it’s important to be honest here.

This kind of approach does not solve:

  • debugger access
  • compromised devices
  • memory dumps
  • perfect string zeroization
  • absolute runtime secrecy

That would be an exaggerated claim.

What it tries to improve is something much more practical:

  • less accidental retention
  • less unintended persistence
  • clearer cleanup rules
  • better expiration handling
  • safer logs
  • more explicit architecture

That already brings real value.

What other state managers solve, and what this is trying to solve

I’m not trying to turn this into a Riverpod vs Bloc vs MobX discussion.

To me, this is a different question.

Most state managers solve state observation and UI flow really well.

What I’m exploring here is whether sensitive data lifecycle deserves to be modeled more explicitly inside the architecture.

Not because existing tools are wrong.

But because this concern often ends up fully manual, scattered, and easy to forget.

Why I ended up exploring this in Stasis

Part of this exploration ended up shaping some ideas in the flutter_stasis ecosystem.

The package originally came out of building Ekklesia and refining how I wanted lifecycle, async flow, and UI events to behave.

Then this other concern became hard to ignore.

At some point, it stopped feeling like “just app code” and started feeling like a missing primitive.

So I began experimenting with the idea of a runtime-oriented layer for sensitive data lifecycle.

Not as a replacement for state management.

Just as something that sits next to it and makes this part of the architecture more explicit.

Final thought

State management solves a lot.

But for sensitive values, the most important question is often not just:

what changed?

Sometimes the real question is:

how long should this value live, and who is responsible for cleaning it up?

To me, that feels important enough to be modeled explicitly.

If you’ve dealt with auth-heavy Flutter apps, I’d be curious:

do you treat passwords, OTPs, and tokens as just more state, or do you model their lifecycle separately?


Links

Top comments (0)