Skip to content

Security Features with fasqbloc

fasq_bloc supports all FASQ security features through QueryClient configuration, enabling secure data handling in your Bloc-based applications.

Security features in fasq_bloc include:

  • Secure cache entries with automatic cleanup
  • Encrypted persistence for sensitive data
  • Input validation preventing injection attacks
  • Platform-specific secure key storage

Mark sensitive data to prevent persistence and enable automatic cleanup:

BlocProvider(
create: (_) => QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
options: QueryOptions(
isSecure: true, // Mark as secure
maxAge: Duration(minutes: 15), // Required TTL
staleTime: Duration(minutes: 5),
),
client: context.queryClient, // Use configured client
),
child: BlocBuilder<QueryCubit<String>, QueryState<String>>(
builder: (context, state) {
// Secure data never persisted, cleared on app background
return Text('Token: ${state.data}');
},
),
)
  • Never persisted to disk - Secure entries are memory-only
  • Automatic cleanup - Cleared on app background/termination
  • Strict TTL enforcement - Expired secure entries are immediately removed
  • Not exposed in DevTools - Secure data is hidden from debugging tools

Handle sensitive mutations with security features:

BlocProvider(
create: (_) => MutationCubit<String, String>(
mutationFn: (data) => api.secureMutation(data),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
),
client: context.queryClient, // Use configured client
),
child: BlocBuilder<MutationCubit<String, String>, MutationState<String>>(
builder: (context, state) {
return ElevatedButton(
onPressed: state.isLoading
? null
: () => context.read<MutationCubit<String, String>>().mutate('secure-data'),
child: state.isLoading
? CircularProgressIndicator()
: Text('Secure Mutation'),
);
},
),
)

Configure security features globally using QueryClientProvider:

final secureClient = QueryClient(
config: const CacheConfig(
defaultStaleTime: Duration(minutes: 5),
defaultCacheTime: Duration(minutes: 10),
),
persistenceOptions: const PersistenceOptions(enabled: true),
);
QueryClientProvider(
client: secureClient,
child: const MaterialApp(
home: MyApp(),
),
);

Use the configured QueryClient in your Bloc providers:

class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => QueryCubit<String>(
key: 'secure-data',
queryFn: () => fetchSecureData(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 30),
),
client: context.queryClient, // Use configured client
),
),
BlocProvider(
create: (_) => MutationCubit<String, String>(
mutationFn: (data) => secureMutation(data),
client: context.queryClient, // Use configured client
),
),
],
child: MyScreenContent(),
);
}
}

Here’s a complete example showing security features in a Bloc-based app:

class SecureApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final secureClient = QueryClient(
config: const CacheConfig(
defaultStaleTime: Duration(minutes: 5),
defaultCacheTime: Duration(minutes: 10),
),
persistenceOptions: const PersistenceOptions(enabled: true),
);
return QueryClientProvider(
client: secureClient,
child: const MaterialApp(
home: SecureHomeScreen(),
),
);
}
}
class SecureHomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Secure App')),
body: MultiBlocProvider(
providers: [
// Secure authentication token
BlocProvider(
create: (_) => QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
staleTime: Duration(minutes: 5),
),
client: context.queryClient,
),
),
// Secure user profile
BlocProvider(
create: (_) => QueryCubit<User>(
key: 'user-profile',
queryFn: () => api.getUserProfile(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 30),
staleTime: Duration(minutes: 10),
),
client: context.queryClient,
),
),
// Secure mutation for updating profile
BlocProvider(
create: (_) => MutationCubit<User, User>(
mutationFn: (user) => api.updateUserProfile(user),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
onSuccess: (user) {
// Invalidate user profile query
context.queryClient?.invalidateQuery('user-profile');
},
),
client: context.queryClient,
),
),
],
child: SecureHomeContent(),
),
);
}
}
class SecureHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Display auth token
BlocBuilder<QueryCubit<String>, QueryState<String>>(
builder: (context, state) {
if (state.isLoading) return CircularProgressIndicator();
if (state.hasError) return Text('Error: ${state.error}');
return Text('Token: ${state.data}');
},
),
SizedBox(height: 20),
// Display user profile
BlocBuilder<QueryCubit<User>, QueryState<User>>(
builder: (context, state) {
if (state.isLoading) return CircularProgressIndicator();
if (state.hasError) return Text('Error: ${state.error}');
return Text('User: ${state.data?.name}');
},
),
SizedBox(height: 20),
// Update profile button
BlocBuilder<MutationCubit<User, User>, MutationState<User>>(
builder: (context, state) {
return ElevatedButton(
onPressed: state.isLoading
? null
: () => context.read<MutationCubit<User, User>>().mutate(
User(name: 'Updated Name'),
),
child: state.isLoading
? CircularProgressIndicator()
: Text('Update Profile'),
);
},
),
],
);
}
}
// Good - Use configured client
QueryCubit<String>(
key: 'secure-data',
queryFn: () => fetchData(),
client: context.queryClient,
)
// Bad - Using default client without security config
QueryCubit<String>(
key: 'secure-data',
queryFn: () => fetchData(),
// Missing client parameter
)
// Good - Sensitive data marked as secure
QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
)
// Bad - Sensitive data not marked as secure
QueryOptions(
isSecure: false, // Sensitive data could be persisted
)
// Good - Short TTL for sensitive data
QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15), // Short-lived tokens
)
// Bad - Too long TTL for sensitive data
QueryOptions(
isSecure: true,
maxAge: Duration(hours: 24), // Too long for sensitive data
)
BlocConsumer<QueryCubit<String>, QueryState<String>>(
listener: (context, state) {
if (state.hasError) {
// Handle security-related errors
if (state.error.toString().contains('validation')) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid input detected')),
);
}
}
},
builder: (context, state) {
// Build UI
},
)
  1. Add the security package using the Flutter CLI:
Terminal window
flutter pub add fasq_security

[!TIP] This package integrates with the Fasq core and the bloc adapter.

  1. Wrap your app with QueryClientProvider:
QueryClientProvider(
config: CacheConfig(
defaultStaleTime: Duration(minutes: 5),
defaultCacheTime: Duration(minutes: 10),
),
persistenceOptions: PersistenceOptions(
enabled: true,
encryptionKey: 'your-encryption-key',
),
child: MaterialApp(
home: MyApp(),
),
)
  1. Update existing QueryCubit instances:
// Before
QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
)
// After
QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
),
client: context.queryClient,
)
  1. Update existing MutationCubit instances:
// Before
MutationCubit<String, String>(
mutationFn: (data) => api.mutate(data),
)
// After
MutationCubit<String, String>(
mutationFn: (data) => api.mutate(data),
client: context.queryClient,
)

Client not found:

  • Ensure QueryClientProvider wraps your app
  • Use context.queryClient to access the configured client

Security validation errors:

  • Check query key format (alphanumeric, colon, hyphen, underscore only)
  • Ensure durations are non-negative
  • Verify cache data doesn’t contain functions

Performance issues:

  • Large data (>50KB) is automatically encrypted in isolates
  • Consider reducing cache size for better performance
  • Use appropriate TTL values to prevent memory bloat
ErrorCauseSolution
”Query key must contain only alphanumeric, colon, hyphen, underscore”Invalid query key formatUse valid characters only
”Secure queries must specify maxAge for TTL enforcement”Missing maxAge for secure queryAdd maxAge to QueryOptions
”staleTime must be non-negative”Negative durationUse positive or zero duration
”Cache data cannot be a function or closure”Function in cache dataRemove functions from cached data