Skip to content

🐛 Bug Report: Realtime only emits events with guest / any read permissions #10353

@lgund

Description

@lgund

👟 Reproduction steps

Create a Appwrite Collection with some information like:

Image

After that sample document and change document while you are listen to the changes via Realtime. I've changed the example to a singleton like this:

import 'package:appwrite/appwrite.dart';
import 'package:baupro/repositories/models/appwrite_exception_model.dart';
import 'package:baupro/repositories/models/appwrite_response_model.dart';
import 'package:get_it/get_it.dart';
import 'package:talker_flutter/talker_flutter.dart';

class AppwriteRepositorySingleton {
  static const String appwriteProjectId = '';
  static const String appwriteProjectName = '';
  static const String appwritePublicEndpoint = '';

  final Client client = Client()
      .setProject(appwriteProjectId)
      .setEndpoint(appwritePublicEndpoint)
      .setLocale('de');

  /// Appwrite account instance
  late final Account account;

  /// Appwrite databases instance
  late final Databases databases;

  /// Appwrite realtime instance
  late final Realtime realtime;

  /// Appwrite teams instance
  late final Teams teams;

  /// Supscription
  late RealtimeSubscription realtimeSubscription;

  AppwriteRepositorySingleton._internal() {
    account = Account(client);
    databases = Databases(client);
    realtime = Realtime(client);
    teams = Teams(client);

    realtimeSubscription = realtime.subscribe(['documents']);
    _listenToRealtimeEvents();
  }

  void renewRealtimeSubscription() {
    try {
      realtimeSubscription.close();
    } catch (e, stack) {
      _talker.warning(
        'Error when closing the old real-time subscription: $e',
        e,
        stack,
      );
    }
  }

  /// Closes the realtime subscription on dispose
  void dispose() {
    realtimeSubscription.close();
    _talker.info('Realtime subscription closed on dispose.');
  }

  static final AppwriteRepositorySingleton _instance =
      AppwriteRepositorySingleton._internal();

  /// Singleton instance getter
  factory AppwriteRepositorySingleton() => _instance;

  /////////////////////////////////////
  // Realtime events
  /////////////////////////////////////
  /// Timeout for reconnecting to Realtime
  int get _reconnectTimeout => 5;

  /// Listens to realtime events and handles errors.
  void _listenToRealtimeEvents() {
    realtimeSubscription.stream.listen(
      (data) {
        _talker.debug('Realtime event: ${data.payload}');
      },
      onError: (error, stackTrace) {
        _talker.error('Realtime error: $error', error, stackTrace);
        Future.delayed(Duration(seconds: _reconnectTimeout), () {
          _talker.info('Attempting to resubscribe to Realtime...');
          realtimeSubscription.close();
          realtimeSubscription = realtime.subscribe(['documents']);
          _listenToRealtimeEvents();
        });
      },
      onDone: () {
        _talker.info(
          'Realtime subscription closed. Attempting to resubscribe...',
        );
        Future.delayed(Duration(seconds: 1), () {
          realtimeSubscription = realtime.subscribe(['documents']);
          _listenToRealtimeEvents();
        });
      },
      cancelOnError: false,
    );
  }

  /////////////////////////////////////
  // Logging section
  /////////////////////////////////////
  /// Get the talker instance
  Talker get _talker => GetIt.instance.get<Talker>();

  /// Handles exceptions and displays an error notification.
  void handleAppwriteException(
    Object e,
    StackTrace? stackTrace, {
    String? messagePrefix,
  }) {
    final prefix = messagePrefix != null ? '$messagePrefix: ' : '';
    if (e is AppwriteException) {
      _talker.error(
        '${prefix}AppwriteException caught in AppwriteRepository: ${e.message} (Code: ${e.code}, Type: ${e.type})',
        e,
        stackTrace,
      );
    } else {
      _talker.error(
        '${prefix}Exception caught in AppwriteRepository',
        e,
        stackTrace,
      );
    }
    // TODO: Füge hier ggf. weitere Fehlerbehandlung hinzu (z.B. Benachrichtigungen)
  }

  /// Wraps an Appwrite request in a try-catch block and logs the result.
  Future<AppwriteResponseModel<T>> safeAppwriteRequest<T>(
    Future<T> Function() requestFunction, {
    String? operationName,
  }) async {
    final opName = operationName ?? 'Appwrite Operation';
    try {
      _talker.info('$opName started.');
      final result = await requestFunction();
      _talker.info('$opName successful.');
      return AppwriteResponseModel(response: result, exception: null);
    } catch (e, stack) {
      handleAppwriteException(e, stack, messagePrefix: '$opName failed');
      return AppwriteResponseModel(
        response: null,
        exception: AppwriteExceptionModel(
          message: e is AppwriteException ? e.message : e.toString(),
          code: e is AppwriteException ? e.code : null,
          type: e is AppwriteException ? e.type : null,
        ),
      );
    }
  }

  /////////////////////////////////////
  // Get Methods
  /////////////////////////////////////
  /// Pings the Appwrite server and captures the response.
  Future<AppwriteResponseModel<bool>> checkConnection() async =>
      await safeAppwriteRequest(() async {
        final response = await client.ping();
        return response.isNotEmpty;
      }, operationName: 'Check Connection');
}

👍 Expected behavior

I should get the information on a change cause of the talker log _talker.debug('Realtime event: ${data.payload}');.

👎 Actual Behavior

Just if I set the collection permission to Any => Read I got this information:

Image
flutter: │ [debug] | 16:37:12 394ms | Realtime event: {Firstname: XXX, Lastname: XXX, Position: XXX, Email: XXX, Active: true, StartWork: null, EndWork: null, BreakTime: null, $id: 68a6f98241f21b4b68d4, $createdAt: 2025-08-21T10:48:25.949+00:00, $updatedAt: 2025-08-21T14:37:03.903+00:00, $permissions: [read("team:68a5837e0027b2a5151e"), update("team:68a5837e0027b2a5151e"), delete("team:68a5837e0027b2a5151e")], Company: {Name: First-Coder, Active: true, TeamId: 68a5837e0027b2a5151e, $id: 68a582570010bb3402be, $createdAt: 2025-08-20T08:07:43.063+00:00, $updatedAt: 2025-08-20T11:14:57.254+00:00, $permissions: [read("team:68a5837e0027b2a5151e"), update("team:68a5837e0027b2a5151e")], Address: {Street: Teststreifen, HouseNumber: 12345, ZipCode: 12345, City: Berlin, Country: Deutschland, $id: 688874740032810f50f9, $createdAt: 2025-07-29T07:12:47.357+00:00, $updatedAt: 2025-07-29T07:12:47.357+00:00, $permissions: [], $databaseId: 687e537b00041c7db62a, $collectionId: 688872a600297a5ac6ba}, $databaseId: 687e537b00041c7db62a, $collectionId: 68a4b21a0005c851c6c3}, $databaseId: 687e537b00041c7db62a, $collectionId: 688871f40011c1886efe}

When I just have the team permissions active I don´t get any realtime messages. But the account is part of the team. I've also have tried to renew the subscription after login like in the sample above without success.

🎲 Appwrite version

Different version (specify in environment)

💻 Operating system

Linux

🧱 Your Environment

Appwrite version: 1.7.4
Flutter version: appwrite: ^16.1.0
Development on macOS

👀 Have you spent some time to check if this issue has been raised before?

I am not sure if the issue #8925

🏢 Have you read the Code of Conduct?

Metadata

Metadata

Assignees

Labels

api / realtimeFixes and upgrades for the Appwrite Realtime API.bugSomething isn't workingproduct / self-hostedIssues only found when self-hosting AppwritequestionFurther information is requested

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions