Notification Templates Admin
This document explains how notification templates work and how to manage them in the admin panel.
Runtime delivery (quick overview)
- Bell + message badges: The SPA uses a unified endpoint (
GET /api/notifications/unified,auth:sanctum+verified) for counts, plus Echo/Reverb for real-time updates. - Bell list loading: The bell notification list is loaded when the user visits the
/notificationspage (counts are maintained elsewhere). - Real-time events: The per-user private channel (
App.Models.User.{id}) broadcastsNotificationCreated(new bell item) andNotificationRead(read state sync across tabs/devices). Messaging updates useMessageSent. - Chat digests: Unread messages are batched and sent via email every 15 minutes (configurable via scheduler) to reduce email fatigue.
- Birthday reminders: The daily pet birthday command is idempotent for bell notifications per user, pet, and day, so accidental double scheduler runs do not create duplicate in-app birthday items.
- Unread message count:
unread_message_countrepresents total unread messages across chats (not "unread chats"). The legacyGET /api/msg/unread-countendpoint is kept for compatibility but aligns with this naming. - Device push: Web push notifications are handled separately; see
docs/push-notifications.md. - Telegram: Users can link their Telegram account to receive notifications via bot; see Telegram Notifications below.
- Notification actions: Bell notifications can include actionable buttons (e.g., approve/unapprove) that execute server-side actions directly from the notification list.
- Operational admin alerts: Some moderation-style events use direct in-app notifications instead of templated multi-channel delivery. Today that includes city creation alerts for
adminandsuper_admin, plus helper profile create/update alerts forsuper_adminonly.
Overview
Notifications support multiple channels (Email, In-App/Bell, and Telegram) and localized templates. Defaults live in the repo as files; admins can create DB overrides per type/channel/locale. The system resolves templates with locale fallbacks (user -> Accept-Language -> app default -> en).
- Types registry:
backend/config/notification_templates.php - DB model:
backend/app/Models/NotificationTemplate.php - Services:
NotificationLocaleResolver,NotificationTemplateResolver,NotificationTemplateRenderer - In-app defaults:
backend/resources/templates/notifications/bell/{locale}/{slug}.md - Email defaults: Blade views at
emails.notifications.{locale}.{slug}(legacyemails.notifications.{slug})
Admin Panel
Path: Admin → Notifications → Templates
List view
- Shows type, channel, locale, engine, status, version, updated at
- Filters: channel, locale, type (slug labels)
- Empty state: Explains that file defaults are in use; “Create override” CTA
Create/Edit
- Type (event trigger): Select populated from registry, filtered by selected channel
- Channel: Email or In-App
- Locale: defaults to app default
- Engine: Blade/Markdown/Text
- Subject (optional, email only)
- Body (required)
- Available variables: Lists variables from the registry entry
- Prefill from defaults: Selecting type/channel/locale preloads body/engine from the file defaults
Actions
- Preview: Renders current draft
- Email: HTML preview
- In-App: pre-wrapped plain text preview
- Compare with Default: Side-by-side DB vs file default for subject/body
- Reset to Default: Deletes override so file default applies again
- Send Test Email: Sends rendered email to the logged-in admin (email channel)
- Preview: Renders current draft
How resolution works
- DB override (active) for type/channel/locale
- File default for type/channel using locale fallback chain
- Final safety fallback logs a warning
Direct In-App Operational Alerts
Some internal admin alerts bypass the template registry and call NotificationService::sendInApp() directly because they are operational review cues rather than user-facing communications.
- City creation sends
city_creatednotifications to users withadminorsuper_adminroles. - Helper profile creation sends
helper_profile_creatednotifications to users with thesuper_adminrole. - Helper profile updates sent through the public API send
helper_profile_updatednotifications to users with thesuper_adminrole.
These alerts still enter the normal bell pipeline, are persisted in the notifications table, and can participate in real-time updates and bell counts.
Seeding
A small seeder adds two inactive in-app overrides so the list isn’t empty on fresh setups:
backend/database/seeders/NotificationTemplateSeeder.php- Registered in
DatabaseSeeder - Uses enum values from
App\\Enums\\NotificationType
Run it:
php backend/artisan db:seed --class=NotificationTemplateSeeder --forceTelegram Notifications
Users can link their Telegram account to receive notification messages from a bot configured by an admin.
Admin setup
- Create a Telegram bot via @BotFather and obtain the bot token.
- Configure the runtime bot values in
backend/.env:TELEGRAM_USER_BOT_TOKEN,TELEGRAM_USER_BOT_USERNAME, and an optionalTELEGRAM_USER_BOT_WEBHOOK_SECRET_TOKENfor webhook authenticity checks. - Register or refresh the webhook so Telegram forwards messages to your app:
cd backend
php artisan telegram:set-webhookThe webhook endpoint (POST /api/webhooks/telegram) is public and handles incoming /start commands from users. When TELEGRAM_USER_BOT_WEBHOOK_SECRET_TOKEN is configured, the app rejects requests unless Telegram sends a matching X-Telegram-Bot-Api-Secret-Token header. Re-run php artisan telegram:set-webhook after changing the secret so Telegram stores the new value.
User linking flow
- User opens Settings > Notifications and clicks Connect Telegram.
- The frontend calls
POST /api/telegram/link-tokenwhich generates a short-lived token (30 min) and returns at.medeep link:https://t.me/<bot>?start=<token>. - The link opens in a new tab. When the user sends
/start <token>to the bot, the webhook:- Validates the token and expiry.
- Stores the Telegram
chat_idon the user record. - Sends a confirmation message back via the bot.
- The frontend polls
GET /api/telegram/statusevery 3 seconds (up to 5 minutes) to detect the connection. - Once connected, the user sees a Disconnect button which calls
DELETE /api/telegram/disconnect.
If a user sends /start without a token, the bot replies with a guidance message directing them to the app's notification settings page.
Mini App sign-in flow
- Telegram Mini App users authenticate via
POST /api/auth/telegram/miniappusingTelegram.WebApp.initData. - This flow is for account authentication/registration; notification delivery still uses bot chat linking (
/start <token>) andtelegram_chat_id. - Existing users can continue linking/unlinking Telegram from Settings > Notifications without changing this behavior.
- Web push (device notifications) may be unavailable in Telegram Mini App webviews; the settings UI shows a Telegram-specific hint and recommends Telegram channel notifications in that context.
Per-type preferences
Each notification type has a telegram_enabled toggle alongside email_enabled and in_app_enabled. The NotificationService checks this preference before dispatching via TelegramNotificationChannel.
Key files
| Purpose | Path |
|---|---|
| Channel implementation | app/Services/Notifications/TelegramNotificationChannel.php |
| Webhook handler | app/Http/Controllers/Telegram/TelegramWebhookController.php |
| Status / link / disconnect | app/Http/Controllers/Telegram/Get*.php, Generate*.php, Disconnect*.php |
| Migration | database/migrations/2026_02_16_000000_add_telegram_support.php |
| Frontend card | frontend/src/components/notifications/TelegramNotificationsCard.tsx |
| Admin settings | app/Filament/Pages/SystemSettings.php (Telegram section) |
Database fields
users table: telegram_chat_id, telegram_link_token, telegram_link_token_expires_atnotification_preferences table: telegram_enabled (boolean, default false)
Notes & Next Steps
- Diff highlighting in Compare (currently plaintext side-by-side)
- Send Test: choose target user + sample payloads
- CLI:
notifications:sync-templatesto validate and diff DB vs file defaults - Tests: unit (resolver/renderer/locale), feature (admin CRUD + preview/compare/reset/send-test), integration (override precedence and metadata)