HosteDay Flutter

pub package platform license

A Flutter SDK for connecting applications to HosteDay authentication, custom APIs, persistent sessions, user management, avatar uploads, and realtime services.

hosteday_flutter provides a high-level API for:

  • Initializing a HosteDay project.
  • Signing users in and out.
  • Creating user accounts.
  • Persisting authenticated sessions.
  • Sending password-reset and email-verification requests.
  • Reading and updating the current user.
  • Uploading the current user's avatar.
  • Calling public and protected custom APIs.
  • Listing, showing, creating, updating, and deleting resources.
  • Connecting to public, private, presence, and encrypted realtime channels.
  • Publishing realtime events through HosteDay.

Flutter Example App


HosteDay platform

HosteDay is a backend and API platform that gives each project an isolated runtime, database, generated API, HTTPS domain, authentication endpoints, file storage, and optional realtime services.

A typical workflow is:

  1. Create a project from the HosteDay dashboard.
  2. Define tables such as posts, orders, or products.
  3. Generate and configure the project API.
  4. Copy the project domain.
  5. Copy the project API key only when project-level API protection is enabled.
  6. Copy the realtime values when realtime is required.
  7. Initialize hosteday_flutter before calling runApp.

The project API key and the signed-in user's access token are different credentials:

Credential Purpose Header
Project API key Identifies or protects the HosteDay project X-Api-Token by default
User access token Authenticates the signed-in user Authorization: Bearer ...

Do not use one credential in place of the other.


Installation

Add the package to a Flutter application:

flutter pub add hosteday_flutter

Import the public library:

import 'package:hosteday_flutter/hosteday_flutter.dart';

For avatar selection from a gallery or camera, add image_picker to the application:

flutter pub add image_picker

The SDK accepts image bytes and does not force applications to use a specific file picker.

Local package development

An example application inside the package repository can depend on the package through a local path:

dependencies:
  flutter:
    sdk: flutter

  hosteday_flutter:
    path: ../

Run dependency resolution from the example directory, not by pasting YAML into the terminal:

cd example
flutter pub get

If recent local package changes do not appear, run:

flutter clean
flutter pub get
flutter run

Quick start

Initialize HosteDay before running the Flutter application:

import 'package:flutter/material.dart';
import 'package:hosteday_flutter/hosteday_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await HosteDay.initializeApp(
    options: const <String, Object?>{
      HosteDayOptionKeys.projectDomain:
      'your-project.hosteday.com',
    },
    authStorage: HosteDaySharedPreferencesAuthStorage(),
  );

  runApp(const App());
}

When project-level API protection is enabled, also provide the project API key:

await
HosteDay.initializeApp
(
options: const <String, Object?>{
HosteDayOptionKeys.projectDomain:
'your-project.hosteday.com',
HosteDayOptionKeys.projectApiKey:
'YOUR_PROJECT_API_KEY',
},
authStorage
:
HosteDaySharedPreferencesAuthStorage
(
)
,
);

Do not configure a placeholder such as YOUR_PROJECT_ACCESS_TOKEN in a real application. Omit projectApiKey when project protection is disabled.


Configuration options

HosteDayOptionKeys contains only values that an application developer is allowed to provide during SDK initialization.

Authentication and user API paths are owned by HosteDay and are not configurable initialization options.

Connection options

Option Description
HosteDayOptionKeys.projectDomain HosteDay project domain, for example enterprise.hosteday.com.
HosteDayOptionKeys.apiBaseUrl Optional complete API base URL. Usually derived from the project domain.
HosteDayOptionKeys.baseUrl Alias for apiBaseUrl.
HosteDayOptionKeys.projectApiKey Optional project API key. Null or empty values are not sent.
HosteDayOptionKeys.apiTokenHeader Optional project key header name. Default: X-Api-Token.
HosteDayOptionKeys.legacyApiToken Legacy key kept for backward compatibility.

Realtime options

Option Description
HosteDayOptionKeys.realtimeAppKey Pusher-compatible application key found after /app/ in the WebSocket URL.
HosteDayOptionKeys.realtimeHost Realtime WebSocket host, for example ws3.hosteday.com.
HosteDayOptionKeys.realtimeScheme ws or wss. Default: wss.
HosteDayOptionKeys.realtimePort Realtime port. Usually 443 with wss.
HosteDayOptionKeys.broadcastingAuthPath Private and presence channel authorization endpoint.
HosteDayOptionKeys.publicEventsPath Public realtime event publishing endpoint.
HosteDayOptionKeys.privateEventsPath Private realtime event publishing endpoint.
HosteDayOptionKeys.presenceEventsPath Presence realtime event publishing endpoint.

Request values such as search, id, relationField, and relationValue are not initialization configuration. They are passed directly to request methods. The SDK writes their backend names into the URL automatically.

For example, application code uses:

await
HosteDay.client.get
('/services
'
,search: 'booking',
relationField: 'company_id',
relationValue
:
15
,
);

The SDK builds a URL equivalent to:

https://your-project.hosteday.com/api/services?search=booking&relation_field=company_id&relation_value=15

The application developer does not need to type relation_field or relation_value manually.


Initialization with realtime

await
HosteDay.initializeApp
(
options: const <String, Object?>{
HosteDayOptionKeys.projectDomain:
'your-project.hosteday.com',
HosteDayOptionKeys.realtimeAppKey:
'YOUR_REALTIME_APP_KEY',
HosteDayOptionKeys.realtimeHost:
'ws3.hosteday.com',
HosteDayOptionKeys.realtimeScheme: 'wss',
HosteDayOptionKeys.realtimePort: 443,
},
authStorage
:
HosteDaySharedPreferencesAuthStorage
(
)
,
);

Connect when realtime is needed:

await
HosteDay.connectRealtime
();

To connect immediately during initialization:

await
HosteDay.initializeApp
(
options: const <String, Object?>{
HosteDayOptionKeys.projectDomain:
'your-project.hosteday.com',
HosteDayOptionKeys.realtimeAppKey:
'YOUR_REALTIME_APP_KEY',
HosteDayOptionKeys.realtimeHost:
'ws3.hosteday.com',
},
authStorage: HosteDaySharedPreferencesAuthStorage(),
connectRealtime: true,
);

Environment variables

Compile-time environment variables are useful for examples and local development:

abstract final class AppEnvironment {
  static const String projectDomain = String.fromEnvironment(
    'HOSTEDAY_PROJECT_DOMAIN',
    defaultValue: 'project.hosteday.com',
  );

  static const String projectApiKey = String.fromEnvironment(
    'HOSTEDAY_PROJECT_API_KEY',
  );

  static const String realtimeAppKey = String.fromEnvironment(
    'HOSTEDAY_REALTIME_APP_KEY',
  );

  static const String realtimeHost = String.fromEnvironment(
    'HOSTEDAY_REALTIME_HOST',
    defaultValue: 'ws3.hosteday.com',
  );

  static const String realtimeScheme = String.fromEnvironment(
    'HOSTEDAY_REALTIME_SCHEME',
    defaultValue: 'wss',
  );

  static const int realtimePort = int.fromEnvironment(
    'HOSTEDAY_REALTIME_PORT',
    defaultValue: 443,
  );
}

Run the application:

flutter run \
  --dart-define=HOSTEDAY_PROJECT_DOMAIN=your-project.hosteday.com \
  --dart-define=HOSTEDAY_PROJECT_API_KEY=your_project_api_key \
  --dart-define=HOSTEDAY_REALTIME_APP_KEY=your_realtime_app_key \
  --dart-define=HOSTEDAY_REALTIME_HOST=ws3.hosteday.com

The first argument of String.fromEnvironment is the environment variable name, not the domain itself.


Public SDK architecture

Use HosteDay and HosteDayClient as the public API:

HosteDay.client;HosteDay.auth;HosteDay.config;HosteDay.realtime;HosteDay.isInitialized;

HosteDayClient is the high-level facade. It owns and coordinates:

  • Authentication.
  • The effective user token provider.
  • HTTP requests.
  • Realtime connections.
  • Realtime publishing helpers.
  • Resource cleanup.

HosteDayHttpClient is a low-level implementation detail used internally by HosteDayClient. Applications should not instantiate it, import it directly, or export it from the package's main public barrel file.

The public barrel should expose the high-level API and public models, for example:

export 'src/auth/hosteday_auth.dart';
export 'src/auth/hosteday_auth_storage.dart';
export 'src/auth/hosteday_shared_preferences_auth_storage.dart';
export 'src/auth/hosteday_token_provider.dart';
export 'src/auth/hosteday_user.dart';
export 'src/config/hosteday_config.dart';
export 'src/config/hosteday_option_keys.dart';
export 'src/exceptions/hosteday_exception.dart';
export 'src/hosteday.dart';
export 'src/hosteday_client.dart';
export 'src/realtime/hosteday_channel_type.dart';
export 'src/realtime/hosteday_realtime_client.dart';
export 'src/realtime/hosteday_realtime_event.dart';

Do not include this internal export:

// Internal implementation; do not export publicly.
// export 'src/http/hosteday_http_client.dart';

Keeping the low-level client internal prevents duplicate public APIs and lets the package change request internals without breaking application code.


Authentication

Authentication state gate

Use authStateChanges() to switch between signed-in and signed-out screens:

class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<HosteDayUser?>(
      stream: HosteDay.auth.authStateChanges(),
      initialData: HosteDay.auth.currentUser,
      builder: (context, snapshot) {
        final user = snapshot.data;

        if (user == null) {
          return const SignInPage();
        }

        return const HomePage();
      },
    );
  }
}

Do not use HosteDay.auth.currentUser!.hasEmail to decide whether a user is authenticated. currentUser can be null while the session is loading. Check whether the user object is null and react to the authentication stream.

Sign in with email and password

try {
final credential =
await HosteDay.auth.signInWithEmailAndPassword(
email: 'user@example.com',
password: 'password123',
);

final user = credential.user;

print(user.id);
print(user.email);
} on HosteDayException catch (error) {
print(error.displayMessage);
}

After successful sign in, HosteDay automatically:

  • Saves the session.
  • Saves the user access token.
  • Updates currentUser.
  • Emits an authentication-state change.
  • Uses the token when withAuth: true is requested.
  • Uses the token to authorize private and presence channels.

Do not print the access token or complete request headers in production logs.

Future<void> signIn(BuildContext context) async {
  await HosteDay.auth.signInWithEmailAndPassword(
    email: emailController.text.trim(),
    password: passwordController.text,
  );

  if (!context.mounted) {
    return;
  }

  Navigator.of(context).pushAndRemoveUntil(
    MaterialPageRoute<void>(
      builder: (_) => const HomePage(),
    ),
        (route) => false,
  );
}

Register a new user

try {
final credential =
await HosteDay.auth.createUserWithEmailAndPassword(
email: 'new-user@example.com',
password: 'password123',
additionalData: <String, dynamic>{
'name': 'Mustafa',
},
);

print(credential.user.id);
print(credential.user.displayName);
} on HosteDayException catch (error) {
print(error.displayMessage);
}

Additional registration values belong inside additionalData:

await
HosteDay.auth.createUserWithEmailAndPassword
(
email: 'new-user@example.com',
password: 'password123',
additionalData: <String, dynamic>{
'name': 'Mustafa',
'phone': '+9647700000000',
},
);

After a successful registration that creates an authenticated session, an application can navigate to HomePage with the same pushAndRemoveUntil pattern used after sign in.

Current user


final user = HosteDay.auth.currentUser;

if (
user == null) {
print('No user is signed in.');
} else {
print(user.id);
print(user.displayName);
print(user.email);
print(user.emailVerified);
print(user.photoUrl);
print(user.avatarUrl);
}

avatarUrl is an alias for photoUrl.

Listen to user changes


final subscription = HosteDay.auth.userChanges().listen((user) {
  if (user == null) {
    print('User signed out.');
    return;
  }

  print('Current user: ${user.email}');
  print('Avatar: ${user.avatarUrl}');
});

Cancel the subscription when it is no longer needed:

await
subscription.cancel
();

Reload the user

Use reload() after a profile update, email verification, or whenever fresh server data is required:

try {
final user = await HosteDay.auth.reload();

print(user.displayName);
print(user.emailVerified);
print(user.avatarUrl);
} on HosteDayException catch (error) {
print(error.displayMessage);
}

Update the user profile

try {
final user = await HosteDay.auth.updateProfile(
name: 'Mustafa Max',
);

print(user.displayName);
} on HosteDayException catch (error) {
print(error.displayMessage);
}

Upload a user avatar

updateAvatar() accepts raw image bytes and one of these extensions:

jpg
jpeg
png
webp

Example using image_picker:

import 'package:hosteday_flutter/hosteday_flutter.dart';
import 'package:image_picker/image_picker.dart';

final ImagePicker imagePicker = ImagePicker();

Future<HosteDayUser?> selectAndUploadAvatar() async {
  final image = await imagePicker.pickImage(
    source: ImageSource.gallery,
    imageQuality: 85,
    maxWidth: 1600,
    maxHeight: 1600,
  );

  if (image == null) {
    return null;
  }

  final bytes = await image.readAsBytes();

  if (bytes.isEmpty) {
    throw StateError('The selected image is empty.');
  }

  final dotIndex = image.name.lastIndexOf('.');

  if (dotIndex == -1 || dotIndex == image.name.length - 1) {
    throw const FormatException(
      'The selected image has no valid extension.',
    );
  }

  final extension = image.name
      .substring(dotIndex + 1)
      .trim()
      .toLowerCase();

  return HosteDay.auth.updateAvatar(
    bytes: bytes,
    extension: extension,
  );
}

The SDK converts the bytes to Base64. Do not add a prefix such as data:image/png;base64, yourself.

The backend may store a relative path:

users/USER_ID/IMAGE.png

The API resource should return a complete HTTPS URL:

{
  "avatar": "https://project.hosteday.com/users/USER_ID/IMAGE.png"
}

Display the avatar:


final avatarUrl = user.avatarUrl;

CircleAvatar
(
radius: 48,
backgroundImage: avatarUrl != null && avatarUrl.isNotEmpty
? NetworkImage(avatarUrl)
    : null,
child: avatarUrl == null || avatarUrl.isEmpty
? const Icon(Icons.person)
    : null,
);

Platform configuration

For Android camera access, add this inside the <manifest> element in android/app/src/main/AndroidManifest.xml:


<uses-permission android:name="android.permission.CAMERA" />

For iOS, add these entries to ios/Runner/Info.plist:


<key>NSPhotoLibraryUsageDescription</key><string>Select a profile picture.</string>

<key>NSCameraUsageDescription</key><string>Take a profile picture.</string>

Web gallery selection uses the browser file picker. Camera capture depends on browser capabilities and user permission.

Send email verification

await
HosteDay.auth.sendEmailVerification
();

Verification is completed through the web link sent to the user.

Send a password-reset email

await
HosteDay.auth.sendPasswordResetEmail
(
email
    :
'
user@example.com
'
,
);

Password reset is completed through the web link sent to the user.

Sign out

await
HosteDay.auth.signOut
();

Signing out clears the local session, emits null through authentication streams, and disconnects realtime. The local session is cleared even when the remote sign-out request fails.


Session storage

Persistent storage

Use shared-preferences storage for normal applications:

await
HosteDay.initializeApp
(
options: const <String, Object?>{
HosteDayOptionKeys.projectDomain:
'your-project.hosteday.com',
},
authStorage
:
HosteDaySharedPreferencesAuthStorage
(
)
,
);

The authenticated session is restored after the application restarts.

Memory storage

Use in-memory storage for tests or temporary sessions:

await
HosteDay.initializeApp
(
options: const <String, Object?>{
HosteDayOptionKeys.projectDomain:
'your-project.hosteday.com',
},
authStorage
:
MemoryHosteDayAuthStorage
(
)
,
);

Memory storage is cleared when the application process stops.

Custom storage

Applications may implement HosteDayAuthStorage when a different persistence mechanism is required:

class CustomAuthStorage implements HosteDayAuthStorage {
  final Map<String, String> _values = <String, String>{};

  @override
  Future<String?> read(String key) async {
    return _values[key];
  }

  @override
  Future<void> write(String key, String value) async {
    _values[key] = value;
  }

  @override
  Future<void> delete(String key) async {
    _values.remove(key);
  }
}

HTTP requests

Use HosteDay.client for custom API requests. The client handles URL building, headers, optional bearer authentication, JSON encoding and decoding, timeouts, validation errors, and network error conversion.

When the configured base URL already ends with /api, pass a resource path such as /posts, not /api/posts.

await
HosteDay.client.get
('/posts
'
);

This resolves to:

https://your-project.hosteday.com/api/posts

Public and protected routes

Custom request methods default to withAuth: false.

Public route:


final response = await
HosteDay.client.get
('/posts
'
);

Protected route:


final response = await
HosteDay.client.get
('/posts
'
,withAuth:
true
,
);

withAuth: true adds:

Authorization: Bearer USER_ACCESS_TOKEN

Use it only when the backend route is protected by user authentication.

Automatic resource URL parameters

The high-level client accepts Dart-style parameter names and converts them to the backend URL automatically:

Dart parameter URL location Backend name
id Path segment No query key; appended as /{id}
search Query string search
relationField Query string relation_field
relationValue Query string relation_value

Null optional values are omitted. Values are safely encoded by Uri.

Pass relationField and relationValue together. The relation field should contain only letters, numbers, and underscores:

^[a-zA-Z0-9_]+$

GET index

List resources without an ID:


final response = await
HosteDay.client.get
('/posts
'
,search: 'flutter',
relationField: 'user_id',
relationValue
    :
'
019f3c33-3f79-733a-9d57-d949cccc90a6
'
,
);

Generated URL:

/api/posts?search=flutter&relation_field=user_id&relation_value=019f3c33-3f79-733a-9d57-d949cccc90a6

GET show

Show one resource by passing the ID separately:


final response = await
HosteDay.client.get
('/posts
'
,id: 12,
relationField: 'user_id',
relationValue:
'
019f3c33-3f79-733a-
9
d57
-
d949cccc90a6
'
,
);

Generated URL:

/api/posts/12?relation_field=user_id&relation_value=019f3c33-3f79-733a-9d57-d949cccc90a6

An index request has no id. A show request includes id.

POST create

Request body values remain inside body:


final response = await
HosteDay.client.post
('/posts
'
,body: <String, dynamic>{
'title': 'New post',
'body': 'Created from the Flutter application.',
'user_id': '019f3c33-3f79-733a-9d57-d949cccc90a6',
},
);

Example Laravel validation:

{
  "body": "required|string|min:10",
  "title": "required|string|max:255",
  "user_id": "sometimes|string|max:255"
}

user_id is part of the JSON body. If it is optional and empty, omit it from the map rather than sending an empty string:


final body = <String, dynamic>{
  'title': title,
  'body': postBody,
  if (userId.isNotEmpty) 'user_id': userId,
};

PUT update

The ID is appended to the path. Relation parameters are placed in the URL, and editable post values stay inside the JSON body:


final response = await
HosteDay.client.put
('/posts
'
,id: 12,
relationField: 'user_id',
relationValue: '019f3c33-3f79-733a-9d57-d949cccc90a6',
body: <String, dynamic>{
'title': 'Updated title',
'body': 'Updated post body with enough characters.',
'user_id': '019f3c33-3f79-733a-9d57-d949cccc90a6',
},
);

Generated URL:

/api/posts/12?relation_field=user_id&relation_value=019f3c33-3f79-733a-9d57-d949cccc90a6

Example Laravel validation:

{
  "id": [
    "required",
    "integer"
  ],
  "body": "sometimes|string|min:10",
  "title": "sometimes|string|max:255",
  "user_id": "sometimes|string|max:255",
  "relation_field": [
    "required",
    "string",
    "regex:/^[a-zA-Z0-9_]+$/"
  ],
  "relation_value": [
    "required"
  ]
}

Because editable fields use Laravel's sometimes rule, send only fields that the user actually changed:


final updates = <String, dynamic>{
  if (titleChanged) 'title': title,
  if (bodyChanged) 'body': postBody,
  if (userIdChanged) 'user_id': userId,
};

PATCH partial update

PATCH is useful when the backend supports partial updates. It uses the same ID, relation parameters, body, authentication, headers, and timeout behavior as PUT:


final response = await
HosteDay.client.patch
('/posts
'
,id: 12,
relationField: 'user_id',
relationValue: '019f3c33-3f79-733a-9d57-d949cccc90a6',
body: <String, dynamic>{
'title': 'Only the title changed',
},
);

Keep patch() only when the generated backend exposes a PATCH route. Otherwise use put().

DELETE

Delete requests pass the ID and relation values in the URL:


final response = await
HosteDay.client.delete
('/posts
'
,id: 12,
relationField: 'user_id',
relationValue:
'
019f3c33-3f79-733a-
9
d57
-
d949cccc90a6
'
,
);

Generated URL:

/api/posts/12?relation_field=user_id&relation_value=019f3c33-3f79-733a-9d57-d949cccc90a6

Example Laravel validation:

{
  "id": [
    "required",
    "integer"
  ],
  "relation_field": [
    "required",
    "string",
    "regex:/^[a-zA-Z0-9_]+$/"
  ],
  "relation_value": [
    "required"
  ]
}

Raw request

Use request() only when the method must be selected dynamically:


final response = await
HosteDay.client.request
(
method: 'POST',
path: '/posts',
body: <String, dynamic>{
'title': 'Created with a raw request',
'body': 'A complete body with at least ten characters.',
},
);

Prefer get, post, put, patch, and delete in normal application code.


Reading API responses

Different Laravel endpoints may return lists in several shapes:

{
  "data": []
}

Paginated response:

{
  "data": {
    "data": []
  }
}

Named collection:

{
  "posts": []
}

An example application may use a response reader to support all three shapes:

abstract final class ApiResponseReader {
  static List<Map<String, dynamic>> readList(Map<String, dynamic> response,) {
    final data = response['data'];

    if (data is List) {
      return data
          .whereType<Map>()
          .map((item) => Map<String, dynamic>.from(item))
          .toList();
    }

    if (data is Map && data['data'] is List) {
      return (data['data'] as List)
          .whereType<Map>()
          .map((item) => Map<String, dynamic>.from(item))
          .toList();
    }

    if (response['posts'] is List) {
      return (response['posts'] as List)
          .whereType<Map>()
          .map((item) => Map<String, dynamic>.from(item))
          .toList();
    }

    return const <Map<String, dynamic>>[];
  }
}

Convert the list into models and sort it:


final posts = ApiResponseReader.readList(response)
    .map(PostModel.fromJson)
    .toList()
  ..sort(
        (a, b) => b.createdAtText.compareTo(a.createdAtText),
  );

The cascade operator ..sort(...) sorts the created list itself and keeps the same list as the expression result.

Post model with user ID

class PostModel {
  final String id;
  final String userId;
  final String title;
  final String? body;
  final String createdAtText;
  final Map<String, dynamic> data;

  const PostModel({
    required this.id,
    required this.userId,
    required this.title,
    this.body,
    this.createdAtText = '',
    this.data = const <String, dynamic>{},
  });

  factory PostModel.fromJson(Map<String, dynamic> json) {
    return PostModel(
      id: json['id']?.toString() ?? '',
      userId: (json['user_id'] ??
          json['userId'] ??
          json['author_id'] ??
          json['owner_id'])
          ?.toString()
          .trim() ??
          '',
      title: json['title']?.toString() ?? 'Untitled post',
      body: json['body']?.toString(),
      createdAtText:
      (json['created_at'] ?? json['createdAt'])?.toString() ?? '',
      data: Map<String, dynamic>.from(json),
    );
  }
}

If the backend validates the ID as an integer, the update page can accept an Object ID or convert a numeric string before sending it:


final postId = int.tryParse(post.id);

if (
postId == null) {
throw const FormatException('Post ID must be an integer.');
}

Realtime

Realtime is available through:

HosteDay.realtime;HosteDay.client.realtime;

Connect and disconnect

await
HosteDay.connectRealtime
();
await
HosteDay.disconnectRealtime
();

Check the current status:


final connected = HosteDay.realtime.isConnected;

Inspect the configured URL:

print
(
HosteDay
.
config
.
realtimeUrl
);

Example:

wss://ws3.hosteday.com:443/app/YOUR_REALTIME_APP_KEY

Public channel

Public channels do not require a signed-in user:

await
HosteDay.connectRealtime
();

final subscription = await
HosteDay.realtime.listenPublic
(
channel: 'posts',
event: 'PostCreated',
onEvent: (event) {
print(event.name);
print(event.channelName);
print(event.payload);
},
);

Cancel the subscription when it is no longer needed:

await
subscription.cancel
();

Private channel

Private channels require a signed-in user:

await
HosteDay.connectRealtime
();

final subscription = await
HosteDay.realtime.listenPrivate
(
channel: 'orders.1',
event: 'OrderUpdated',
onEvent: (event) {
print(event.payload);
print(event.userId);
print(event.userName);
print(event.userEmail);
},
);

The SDK adds the private- prefix when needed.

Presence channel

Presence channels require a signed-in user:

await
HosteDay.connectRealtime
();

final subscription = await
HosteDay.realtime.listenPresence
(
channel: 'chat.room.1',
event: 'MessageSent',
onEvent: (event) {
print(event.payload);
},
);

The SDK adds the presence- prefix when needed.

Listen for members joining or leaving:

await
HosteDay.realtime.listenPresenceMemberAdded
(
channel: 'chat.room.1',
onEvent: (event) {
print(event.payload);
},
);

await HosteDay.realtime.listenPresenceMemberRemoved(
channel: 'chat.room.1',
onEvent: (event) {
print(event.payload);
},
);

Private encrypted channel

Encrypted channels require backend support:

await
HosteDay.realtime.listenPrivateEncrypted
(
channel: 'secure.orders.1',
event: 'SecureOrderUpdated',
onEvent: (event) {
print(event.payload);
},
);

Unified listener

Use listen() when the channel type is selected dynamically:


final subscription = await
HosteDay.realtime.listen
(
channel: 'orders.1',
event: 'OrderUpdated',
type: HosteDayChannelType.private,
onEvent: (event) {
print(event.payload);
},
);

Supported channel types:

HosteDayChannelType.public;HosteDayChannelType.private;HosteDayChannelType
    .presence;HosteDayChannelType.privateEncrypted;

Unsubscribe

await
HosteDay.realtime.unsubscribe
('orders.1
'
,type:
HosteDayChannelType
.
private
,
);

Publishing realtime events

Public event


final response = await
HosteDay.client.publishPublicEvent
(
channel: 'posts',
event: 'PostCreated',
payload: <String, dynamic>{
'post': <String, dynamic>{
'id': 1,
'title': 'New realtime post',
},
},
);

Private event

A signed-in user is required:


final response = await
HosteDay.client.publishPrivateEvent
(
channel: 'orders.1',
event: 'OrderUpdated',
payload: <String, dynamic>{
'order_id': 1,
'status': 'paid',
},
);

Presence event

A signed-in user is required:


final response = await
HosteDay.client.publishPresenceEvent
(
channel: 'chat.room.1',
event: 'MemberTyping',
payload: <String, dynamic>{
'typing': true,
},
);

Realtime event object

Realtime callbacks receive a HosteDayRealtimeEvent:

await
HosteDay.realtime.listenPublic
(
channel: 'posts',
event: 'PostCreated',
onEvent: (event) {
print(event.name);
print(event.channelName);
print(event.payload);
print(event.data);
print(event.message);
print(event.user);
print(event.userId);
print(event.userName);
print(event.userEmail);
},
);

Access payload values directly:


final title = event['title'];
final post = event['post'];

if (
event.containsKey('post')) {
print(event['post']);
}

Error handling

Most API and network failures throw HosteDayException:

try {
final response = await HosteDay.client.get('/posts');
print(response);
} on HosteDayException catch (error) {
print(error.message);
print(error.statusCode);
print(error.displayMessage);
} catch (error) {
print(error);
}

Validation errors

Laravel-style validation responses are exposed through validationErrors:

{
  "message": "The email field is required.",
  "errors": {
    "email": [
      "The email field is required."
    ],
    "password": [
      "The password field is required."
    ]
  }
}
try {
await HosteDay.auth.signInWithEmailAndPassword(
email: email,
password: password,
);
} on HosteDayException catch (error) {
print(error.firstErrorFor('email'));
print(error.firstErrorFor('password'));
print(error.displayMessage);
}

Useful helpers include:

error.hasValidationErrors;error.isValidationError;error.isUnauthenticated;error.isForbidden;error
    .isNotFound;error.isServerError;error.firstValidationError;error.displayMessage;

Unauthenticated. with status 401

A visible user in the Flutter interface does not prove that a custom backend route accepts the current token. Check these cases:

  1. The request uses the correct project domain and API base URL.
  2. The backend route is actually protected by user authentication.
  3. withAuth: true is used only for protected routes.
  4. The user token belongs to the same HosteDay project and authentication system as the target API.
  5. The token has not expired or been revoked.
  6. The backend guard accepts the token type issued by the login endpoint.
  7. Redirects or proxies do not remove the Authorization header.

If the custom route is public, call it with the default withAuth: false:

await
HosteDay.client.post
('/posts
'
,body: <String, dynamic>{
'title': 'Public post',
'body': 'A public post body with enough characters.',
},
);

If a public route still returns Unauthenticated., the authentication failure is coming from backend route configuration, middleware, controller code, model observers, policies, or a service called by that route. Inspect the backend route and middleware list.

The X-Api-Token project key does not replace the bearer token. Removing or adding it cannot fix an incompatible user authentication guard.

Never publish or log the complete bearer token. Revoke any token that has been shared publicly.

Data too long for column 'body'

This database error means the request passed application validation but the database column is too short for the submitted text. A VARCHAR(255) column cannot store a long post body.

Use a TEXT or LONGTEXT column when long bodies are allowed:

Schema::table('posts', function (Blueprint $table) {
    $table->text('body')->change();
});

Also align request validation with the database capacity, for example:

'body' => ['required', 'string', 'min:10', 'max:10000'],

Fixed HosteDay endpoints

Authentication and user endpoints are managed internally by HosteDay and are not initialization options:

POST   /api/auth/login
POST   /api/auth/register
POST   /api/auth/forgot-password
GET    /api/user
PUT    /api/user
POST   /api/user/avatar
DELETE /api/user
POST   /api/logout
POST   /api/email/verification-notification

Realtime authorization and publishing endpoints are also managed by the SDK:

POST /api/realtime/events/public
POST /api/realtime/events/private
POST /api/realtime/events/presence
POST /api/broadcasting/auth-manual

Use HosteDay.client for project-specific resources such as posts, orders, and products.


Expected authentication responses

Sign-in response

Recommended shape:

{
  "access_token": "1|example-token",
  "token_type": "Bearer",
  "expires_in": null,
  "user": {
    "id": 1,
    "name": "Mustafa",
    "email": "mustafa@example.com",
    "email_verified_at": "2026-07-08T10:00:00.000000Z",
    "avatar": "https://project.hosteday.com/users/1/avatar.png"
  }
}

The SDK also accepts token, nested data, and common user field aliases.

Supported token keys:

access_token
accessToken
token

Supported user ID keys:

id
user_id
uuid

Supported user name keys:

name
display_name
displayName
full_name
fullName

Supported avatar keys:

avatar_url
avatarUrl
avatar
photo_url
photoUrl
image
image_url
imageUrl

Email verification keys:

email_verified
emailVerified
email_verified_at
emailVerifiedAt

Application navigation example

A common posts flow is:

HomePage
└── Index
    ├── CreatePage
    └── Show
        ├── UpdatePage
        └── Delete confirmation

The index page should focus on loading, displaying, refreshing, and optionally listening for posts. Keep create, update, show, and delete responsibilities in their own pages or widgets.

A PostCard can open the update page and refresh the index after success:


final updated = await
Navigator.of
(
context).push<bool>(
MaterialPageRoute<bool>(
builder: (_) => UpdatePage(
postId: int.parse(post.id),
relationField: 'user_id',
relationValue: post.userId,
),
),
);

if (updated == true) {
await reloadPosts();
}

Example application

A complete example is included in the repository:

example/

It demonstrates:

  • SDK initialization.
  • Persistent authentication sessions.
  • Sign in and registration.
  • Navigation to the home page after authentication.
  • Password-reset and email-verification requests.
  • A shared authentication-aware AppBar.
  • Profile updates.
  • Avatar selection and upload.
  • Full HTTPS avatar display.
  • Posts index, show, create, update, and delete flows.
  • user_id inside create and update request bodies.
  • Automatic id, search, relation_field, and relation_value URL values.
  • Public and protected API calls.
  • Realtime listening and publishing.

See example/README.md for the example-specific architecture and complete page flow.


Migration notes

Low-level HTTP client

Old application code may import or call HosteDayHttpClient directly. New code should call HosteDay.client instead.

Remove this public export:

export 'src/http/hosteday_http_client.dart';

Resource URL construction

Old application code may manually write query names:

queryParameters: <
String, Object?>{
'relation_field': 'user_id',
'relation_value': userId,
}

New code passes values directly:

await
HosteDay.client.get
('/posts
'
,relationField: 'user_id',
relationValue: userId,
);

Old show, update, or delete code may append the ID manually:

await
HosteDay.client.get
('/posts/$postId'
);

New code can pass the ID separately:

await
HosteDay.client.get
('/posts
'
,id:
postId
,
);

Project API token name

Old name:

HosteDayOptionKeys.apiToken

Current name:

HosteDayOptionKeys.projectApiKey

The current name prevents confusion with the signed-in user's access token.

Realtime key name

Old name:

HosteDayOptionKeys.pusherKey

Current name:

HosteDayOptionKeys.realtimeAppKey

HosteDay uses a Pusher-compatible protocol, but application developers do not need a Pusher account.

Global class name

Old name:

Hosteday

Current name:

HosteDay

Use HosteDay in all new code.


Turn your ideas into production-ready applications without spending time on server provisioning, API infrastructure, authentication, databases, file storage, or realtime configuration. HosteDay brings these services together in one developer-friendly platform, so you can focus on building the experience your users need. Create your project and start building today at hosteday.com.


Security notes

  • Do not hard-code production credentials in public repositories.
  • Do not configure placeholder project API keys in production.
  • Do not print complete bearer tokens or authorization headers.
  • Use withAuth: true only for routes protected by user authentication.
  • Validate authorization and relation ownership on the backend.
  • Validate relation field names against an allowlist when possible.
  • Validate avatar MIME type, size, and extension on the backend.
  • Return file and avatar links through HTTPS.
  • Use private or presence realtime channels for sensitive data.
  • Do not treat client-side checks as a security boundary.
  • Revoke tokens that were exposed in logs, screenshots, or messages.

License

MIT

Libraries

hosteday_flutter