Skip to content

Security Features with fasqhooks

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

Security features in fasq_hooks 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:

class SecureDataWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final client = context.queryClient;
final secureQuery = useQuery<String>(
'auth-token',
() => api.getAuthToken(),
options: QueryOptions(
isSecure: true, // Mark as secure
maxAge: Duration(minutes: 15), // Required TTL
staleTime: Duration(minutes: 5),
),
client: client, // Use configured client
);
if (secureQuery.isLoading) return CircularProgressIndicator();
if (secureQuery.hasError) return Text('Error: ${secureQuery.error}');
// Secure data never persisted, cleared on app background
return Text('Token: ${secureQuery.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:

class SecureMutationWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final client = context.queryClient;
final mutation = useMutation<String, String>(
mutationFn: (data) => api.secureMutation(data),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
),
client: client, // Use configured client
);
return ElevatedButton(
onPressed: mutation.isLoading
? null
: () => mutation.mutate('secure-data'),
child: mutation.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 hooks:

class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final client = context.queryClient;
// Use configured client in hooks
final secureQuery = useQuery<String>(
'secure-data',
() => fetchSecureData(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 30),
),
client: client,
);
final mutation = useMutation<String, String>(
mutationFn: (data) => secureMutation(data),
client: client,
);
return Column(
children: [
Text('Data: ${secureQuery.data}'),
ElevatedButton(
onPressed: () => mutation.mutate('data'),
child: Text('Mutate'),
),
],
);
}
}

Here’s a complete example showing security features in a hooks-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 HookWidget {
@override
Widget build(BuildContext context) {
final client = context.queryClient;
// Secure authentication token
final authToken = useQuery<String>(
'auth-token',
() => api.getAuthToken(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
staleTime: Duration(minutes: 5),
),
client: client,
);
// Secure user profile
final userProfile = useQuery<User>(
'user-profile',
() => api.getUserProfile(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 30),
staleTime: Duration(minutes: 10),
),
client: client,
);
// Secure mutation for updating profile
final updateProfile = useMutation<User, User>(
mutationFn: (user) => api.updateUserProfile(user),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
onSuccess: (user) {
// Invalidate user profile query
client.invalidateQuery('user-profile');
},
),
client: client,
);
return Scaffold(
appBar: AppBar(title: Text('Secure App')),
body: Column(
children: [
// Display auth token
if (authToken.isLoading)
CircularProgressIndicator()
else if (authToken.hasError)
Text('Error: ${authToken.error}')
else
Text('Token: ${authToken.data}'),
SizedBox(height: 20),
// Display user profile
if (userProfile.isLoading)
CircularProgressIndicator()
else if (userProfile.hasError)
Text('Error: ${userProfile.error}')
else
Text('User: ${userProfile.data?.name}'),
SizedBox(height: 20),
// Update profile button
ElevatedButton(
onPressed: updateProfile.isLoading
? null
: () => updateProfile.mutate(
User(name: 'Updated Name'),
),
child: updateProfile.isLoading
? CircularProgressIndicator()
: Text('Update Profile'),
),
],
),
);
}
}

Create custom hooks for common security patterns:

// Custom hook for secure authentication
QueryState<String> useSecureAuth() {
final client = useQueryClient();
return useQuery<String>(
'auth-token',
() => api.getAuthToken(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
staleTime: Duration(minutes: 5),
),
client: client,
);
}
// Custom hook for secure user profile
QueryState<User> useSecureUserProfile() {
final client = useQueryClient();
return useQuery<User>(
'user-profile',
() => api.getUserProfile(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 30),
staleTime: Duration(minutes: 10),
),
client: client,
);
}
// Custom hook for secure profile update
MutationState<User> useSecureProfileUpdate() {
final client = useQueryClient();
return useMutation<User, User>(
mutationFn: (user) => api.updateUserProfile(user),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
onSuccess: (user) {
client.invalidateQuery('user-profile');
},
),
client: client,
);
}
// Usage in widgets
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final auth = useSecureAuth();
final profile = useSecureUserProfile();
final updateProfile = useSecureProfileUpdate();
return Column(
children: [
Text('Token: ${auth.data}'),
Text('User: ${profile.data?.name}'),
ElevatedButton(
onPressed: () => updateProfile.mutate(User(name: 'New Name')),
child: Text('Update'),
),
],
);
}
}
// Good - Use configured client
final client = useQueryClient();
final query = useQuery<String>(
'secure-data',
() => fetchData(),
client: client,
)
// Bad - Using default client without security config
final query = useQuery<String>(
'secure-data',
() => 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
)
class SecureWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final client = useQueryClient();
final query = useQuery<String>(
'secure-data',
() => fetchData(),
client: client,
);
// Handle security-related errors
useEffect(() {
if (query.hasError) {
if (query.error.toString().contains('validation')) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid input detected')),
);
}
}
return null;
}, [query.hasError]);
return Text('${query.data}');
}
}
  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 hooks 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 useQuery calls:
// Before
final query = useQuery<String>(
'auth-token',
() => api.getAuthToken(),
)
// After
final client = useQueryClient();
final query = useQuery<String>(
'auth-token',
() => api.getAuthToken(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
),
client: client,
)
  1. Update existing useMutation calls:
// Before
final mutation = useMutation<String, String>(
mutationFn: (data) => api.mutate(data),
)
// After
final client = useQueryClient();
final mutation = useMutation<String, String>(
mutationFn: (data) => api.mutate(data),
client: client,
)

Client not found:

  • Ensure QueryClientProvider wraps your app
  • Use useQueryClient() hook 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