gvl_comments — Add comments to any Flutter app

GVL Comments Flutter SDK demo — threaded replies, reactions, instant posting and moderation

pub.dev pub points license flutter

Drop-in comment section for Flutter: threaded replies, reactions, moderation and instant (optimistic) posting, with no backend to build. Powered by GoodVibesLab Cloud.

  • Android & iOS
  • Free plan: 1,000 comments/month, AI moderation included, no credit card (pricing)
  • Data hosted in the EU (Stockholm)

Why gvl_comments?

What you get What you skip
Comments UI (list + composer) Designing a database schema
Threaded replies (depth 2) Writing security rules / RLS
6 emoji reactions Building pagination & cursor logic
AI + user report moderation Rate-limiting and abuse prevention
Cursor-based pagination Token management
Material 3 theming (5 presets) Maintaining backend infrastructure

One install key, zero backend code.


Quick start

1. Get an install key

  1. Sign up at goodvibeslab.cloud (free) and create a project.
  2. In the project's Platforms card, register your app: Android Package name (applicationId) and/or iOS Bundle ID.
  3. Click Add key and copy the install key (cmt_live_…).

Install keys only work for the apps registered on the project. Skipping step 2 makes the first token request fail with api_key_not_bound_to_app.

2. Install

# pubspec.yaml
dependencies:
  gvl_comments: ^1.2.0

3. Initialize

import 'package:gvl_comments/gvl_comments.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await CommentsKit.initialize(
    installKey: const String.fromEnvironment('GVL_INSTALL_KEY'),
  );

  runApp(const MyApp());
}
flutter run --dart-define=GVL_INSTALL_KEY="cmt_live_xxx"

4. Add the widget

CommentsList(
  threadKey: 'post:550e8400-e29b-41d4-a716-446655440000',
  user: UserProfile(id: 'user-1', name: 'Alice'),
)

That's it. Comments load, users post, reactions work — all out of the box.

Using your own auth

Pass the signed-in user from whatever auth you already use. id must be stable for that user (it's how comments, reactions and reports are attributed):

// Firebase Auth
final u = FirebaseAuth.instance.currentUser!;
final user = UserProfile(id: u.uid, name: u.displayName, avatarUrl: u.photoURL);

// Supabase Auth
final s = Supabase.instance.client.auth.currentUser!;
final user = UserProfile(id: s.id, name: s.userMetadata?['name']);

Features

CommentsList

Full-featured comment thread with built-in composer, pagination, and optimistic posting.

CommentsList(
  threadKey: 'article:01HV9ZJ7Q4X2M0YB8K9E',
  user: currentUser,
  newestAtBottom: false,        // feed mode (default) or chat mode
  limit: 30,                    // comments per page (1–100)
  reactionsEnabled: true,       // emoji reaction bar
  shrinkWrap: true,             // embed inside a parent scrollable
  header: MyArticleCard(),      // scrolls with comments
  theme: GvlCommentsThemeData.bubble(context),
)

CommentCount

Lightweight counter — no full list load.

CommentCount(
  threadKey: 'post:550e8400-e29b-41d4-a716-446655440000',
  user: currentUser,
  builder: (context, count, refresh) => Text('$count comments'),
)

TopComment

Display the most-engaged comment (highest reactions) as a preview.

TopComment(
  threadKey: 'post:550e8400-e29b-41d4-a716-446655440000',
  user: currentUser,
  onTap: () => Navigator.push(/* full thread */),
)

Batch prefetch

Avoid N+1 in lists — prefetch counts and top comments for multiple threads at once.

await CommentsKit.I().prefetchThreads(
  posts.map((p) => 'post:${p.id}').toList(),  // e.g. UUIDs from your DB
  user: currentUser,
);
// CommentCount and TopComment now read from cache

Theming

Five built-in presets, all Material 3 compatible:

GvlCommentsThemeData.defaults(context)  // adapts to app theme
GvlCommentsThemeData.neutral(context)   // minimal, clean
GvlCommentsThemeData.compact(context)   // dense, for dashboards
GvlCommentsThemeData.card(context)      // elevated cards
GvlCommentsThemeData.bubble(context)    // chat-style bubbles

Full control via properties:

GvlCommentsThemeData(
  bubbleColor: Colors.blue.shade50,
  avatarSize: 32,
  spacing: 12,
  bubbleRadius: BorderRadius.circular(16),
  authorStyle: TextStyle(fontWeight: FontWeight.bold),
)

Or use Theme.of(context).extension<GvlCommentsThemeData>() for app-wide styling.


Reactions

Six reactions: like, love, laugh, wow, sad, angry.

  • Tap to toggle the default reaction (like)
  • Long-press to open the reaction picker
  • Disable per widget: reactionsEnabled: false

Moderation

Comments pass through a moderation pipeline:

Status Behavior
approved Visible normally
pending Visible to author, placeholder for others when flagged
rejected Replaced by "This comment has been moderated"
  • User reports — from the ⋯ menu on each comment, with a confirmation step; duplicate-safe
  • AI moderation — automatic flagging, on by default on every plan (free included)
  • Configure thresholds and sensitivity from the dashboard

Programmatic API

Full control beyond the widget:

final kit = CommentsKit.I();

// Thread keys must be high-entropy (UUID/ULID/Firestore doc id, 20+ chars).
const threadKey = 'post:3f2b8c1e-9a4d-4e7f-b2c6-1d8e5a7f9b30';

// List with pagination
final page = await kit.listPage(threadKey, user: user);
final comments = page.items;
if (page.hasMore) {
  final next = await kit.listPage(threadKey, user: user, cursor: page.nextCursor);
}

// Post
final comment = await kit.post(
  threadKey: threadKey,
  body: 'Hello!',
  user: user,
  parentId: parentComment.id,  // optional, for replies
);

// React
await kit.setCommentReaction(
  commentId: comment.id,
  reaction: Reaction.love.id,  // or null to remove
  user: user,
);

// Report
final isDuplicate = await kit.report(commentId: id, user: user);

// User identity (tokens are per user: a user switch is detected
// automatically; call invalidateToken() on logout to drop cached data)
await kit.identify(newUser);
kit.invalidateToken();

Builder hooks

Override any part of the UI:

Builder Controls
commentItemBuilder Entire comment row
avatarBuilder Avatar widget
sendButtonBuilder Send button
composerBuilder Full input area
separatorBuilder Dividers between comments
loadMoreButtonBuilder Pagination button

Thread keys

Thread keys identify comment threads. They must be:

  • 20+ characters long
  • High-entropy (UUID, ULID, Firestore doc ID)
  • Characters: a-zA-Z0-9:_-.
post:550e8400-e29b-41d4-a716-446655440000   ✅
article:01HV9ZJ7Q4X2M0YB8K9E               ✅
post-123                                     ❌ guessable

No pre-creation needed — threads are resolved server-side on first use.


Webhooks

Subscribe to events from the dashboard:

Event Trigger
comment.created New comment posted
comment.replied Reply to existing comment
comment.liked Reaction added
comment.mentioned User mentioned in flattened reply

Payloads are signed with HMAC-SHA256. See the webhook docs for verification examples.


Security

  • Short-lived JWTs — tokens expire after 1 hour
  • App binding — lock install keys to Android SHA-256 / iOS Team ID
  • Rate limiting — per IP, per user, per thread
  • Row-Level Security — tenant-isolated data, no cross-project access
  • 15s request timeout — prevents indefinite hangs

Localization

Register the SDK's localization delegates in your MaterialApp:

MaterialApp(
  localizationsDelegates: GvlCommentsL10n.localizationsDelegates,
  supportedLocales: GvlCommentsL10n.supportedLocales,
)

Ships with 5 locales: English, French, Spanish, German, and Portuguese. All UI strings (timestamps, errors, hints, reaction labels) go through the l10n system.


Logging

await CommentsKit.initialize(
  installKey: key,
  logLevel: CommentsLogLevel.trace,  // off | error | info | debug | trace
);

Defaults to error in release, debug in debug mode. Sensitive values (keys, tokens) are redacted.


FAQ

Do I need my own backend or database? No. Comments, reactions, reports and moderation are stored and served by GoodVibesLab Cloud. You only ship the SDK and an install key.

Are new comments pushed in real time? No. Your own comments appear instantly (optimistic UI), and other people's comments show up on pull-to-refresh or when the thread is reopened.

Does it work offline? No. Loading and posting need a network connection. A failed post keeps the user's text and offers a retry.

Which platforms are supported? Android and iOS. Web and desktop are not supported yet.

Where is the data stored? In the EU (Stockholm). Each account's data is isolated by row-level security.

Can I restyle or replace the UI? Yes: use a theme preset, GvlCommentsThemeData properties, or the builder hooks to replace any part (item, avatar, composer, send button…).


Requirements

Minimum
Flutter 3.19
Dart 3.3
iOS 13.0
Android API 24

Example app

git clone https://github.com/GoodVibesLab/gvl_comments.git
cd gvl_comments/example
flutter run

Runs with a built-in demo key. Shows posting, reactions, theming, dark mode, and guest identity.


  • Dashboard — create projects, install keys, configure moderation
  • Documentation — full API reference and guides
  • Issues — bug reports and feature requests
  • Contact — support

License

Commercial license. Included with all GoodVibesLab plans (free tier available).