Dartvel, home
Docs
Features
Studio
Cloud
Compared

Search the site

GitHub
pub.dev

GETTING STARTED

Getting started
Existing Flutter apps
Existing Native apps
Run on your phone

APP

UI and styling
Routing
State
Accessibility
Keyboard shortcuts
Localization
Devices and desktop
Native device access
Media, 3D and XR

DATA

Data models
Forms
Search
Sync and offline
Import and export
Change capture
Database
Cache
File storage
Images
Privacy and erasure

BACKEND

Backend functions
Auth and sessions
Authorization
Queues and jobs
Workers and memory
Notifications and mail
Outbound HTTP
AI
Webhooks
GraphQL and OpenAPI
API keys and OAuth
Multi-tenancy
Billing and commerce
Modules

OPERATIONS

Edge security
Secrets and environments
Monitoring
Releases

SHIPPING

Build targets
Static web hosting
Servers and deploying

REFERENCE

Testing
CLI reference
Coding agents

BACKEND

Notifications and mail

Send email, in-app and push notifications through DV.Notifications.

Mail is part of it, at DV.Notifications.mail.

ON THIS PAGE

Pick a mail provider

Send an email

Notify a person on several channels

Push to Android and the browser

Receive push in the app

Topics and data messages

Status

Pick a mail provider

void configureMail() {
  DV.Notifications.mail.useProvider(SmtpMailProvider(
    host: 'smtp.example.com',
    username: 'apikey',
    password: DV.Secrets.get('SMTP_PASSWORD'),
  ));
}

Copy code to clipboard

SmtpMailProvider

Sends through: Any SMTP server, port 587 by default

ResendMailProvider

Sends through: Resend

SendGridMailProvider

Sends through: SendGrid

PostmarkMailProvider

Sends through: Postmark

SesMailProvider

Sends through: Amazon SES

MailgunMailProvider

Sends through: Mailgun

DVMemoryMailProvider

Sends through: Nowhere. It keeps messages in .sent for tests

Providers are set in code. Read passwords and API keys with DV.Secrets.get.

Send an email

await DV.Notifications.mail.send(const DVMailMessage(
  from: DVMailAddress('hello@example.com', name: 'Shop'),
  to: <DVMailAddress>[DVMailAddress('ada@example.com')],
  subject: 'Your order shipped',
  text: 'It arrives on Friday.',
));

Copy code to clipboard

Add html: for an HTML body next to the text one.

send throws when no mail provider is set.

Notify a person on several channels

DV.Notifications.register(DVMemoryNotificationProvider());

final DVNotificationDelivery delivery = await DV.Notifications.send(
  'user-1',
  const DVNotificationMessage(
    title: 'Order shipped',
    body: 'It arrives on Friday.',
    channels: <DVNotificationChannel>[DVNotificationChannel.inApp],
  ),
);

Copy code to clipboard

Channels are inApp, email, push, sms and webPush.

Register one provider per kind, such as FirebasePushProvider, ApnsPushProvider, WebPushProvider or TwilioSmsProvider.

useRoutes tells the service each person's address and device tokens.

push tries the native providers first and falls back to Web Push when none is registered or the one that is fails.

usePreferences reads each person's muted channels and quiet hours. Push, web push and SMS are not sent during quiet hours, and email is.

send returns a DVNotificationDelivery with the outcome of every channel, so a muted or unrouted channel is told apart from one that failed.

Push to Android and the browser

Name the targets under dartvel.push in pubspec.yaml: web takes the public half of your VAPID key (dartvel push vapid-keys makes a pair), and android.fcm takes the four values the Firebase console shows for the app. The build writes everything else, so nothing in android/ or in a service worker is edited by hand.

The credentials that send are Secrets, never pubspec.yaml: FCM_SERVICE_ACCOUNT_JSON for FCM, VAPID_PRIVATE_KEY and VAPID_SUBJECT for Web Push. The config parser refuses a private key written in pubspec.yaml.

FirebasePushProvider.fromSecrets signs the service-account token request itself, keeps the access token until just before it expires and retries once after a 401. The key is redacted from logs and is in no error message.

void configurePush() {
  // The service-account key (FCM_SERVICE_ACCOUNT_JSON) and the VAPID private
  // key (VAPID_PRIVATE_KEY, VAPID_SUBJECT) are read from DV.Secrets.
  DV.Notifications.register(FirebasePushProvider.fromSecrets());
  DV.Notifications.register(WebPushProvider.fromSecrets());
}

Copy code to clipboard

Receive push in the app

Future<void> enablePush() async {
  // Called from a tap: starting push asks for nothing.
  await PushNotifications.requestPermission();
  final String? token = await PushNotifications.getToken();
  if (token != null) DV.log('push token ready');
  PushNotifications.onMessage.listen((PushNotification note) {
    DV.log('push: ${note.title}');
  });
}

Copy code to clipboard

getToken is an FCM registration token on Android and the browser subscription on the web. Send it to your server, where DVNotificationRoutes takes it as pushTokens or webPushSubscriptions.

Starting push asks for no permission. requestPermission is for a tap: a browser stops showing the prompt after a few dismissals, so asking at load spends the only chance.

On Android the build writes the Firebase service, the JNI bridge, the manifest entry, the Gradle dependency and the string resources Firebase reads, so there is no google-services.json. A message that arrives while the app is not running is kept until Dart asks.

On the web the one generated service worker, shared by web and web-server, shows a notification when no window is visible, hands the message to the page when one is, and opens only pages of your own origin when it is clicked.

Fire OS and Horizon OS have no Google Play services, so their builds leave FCM out.

Topics and data messages

Future<void> announce() async {
  final FirebasePushProvider fcm = FirebasePushProvider.fromSecrets();
  await fcm.sendTopic(
    'news',
    const DVNotificationMessage(title: 'New release', body: 'Version 2 is out.'),
  );
  await fcm.sendData('device-token', <String, String>{'sync': 'orders'});
}

Copy code to clipboard

A topic reaches every device subscribed to it. Web Push has no topics, and subscribeToTopic there says so.

A data message shows nothing; the app receives it and decides.

FCM answers a token it no longer knows with UNREGISTERED. That is isUnregisteredToken on the exception: drop the token. isRetryable is true for quota, unavailable and internal errors.

Status

Partial

Spec section: Mail and Notifications

Planned work and implementation limits

Web Push has been run against Chrome's real push service. FCM has not: no Firebase project or Android device was available, so no token has come from a phone and no message has been delivered to one.

A click on a notification is tested in the worker and has not been tried in a browser. Nor have Firefox and Safari.

A tap on a notification while the Android app is already running in the background is not reported as opened.

No attachments, bounce webhooks or List-Unsubscribe header.

Mail is sent directly, and not through the job queue.

Desktop and TV push kinds have no implementation, and iOS has no device registration yet.

PREVIOUS Workers and memory Heavy work on other cores, and memory reserved up front
NEXT Outbound HTTP Call APIs you have declared, with retries
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom