Creating in-app notifications

In-app notifications are a great way to communicate with your users. They can be used to inform users about new features, promotions, or any other important information. Nextbase Ultimate comes with a built-in in-app notification system that allows you to create and send notifications to your users.

Notifications Table

User notifications are managed in the user_notifications table which is created automatically by Nextbase Ultimate supabase migrations during installation. Here is the relevant part of the table schema:

CREATE TABLE user_notifications (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID REFERENCES user_profiles(id) ON DELETE CASCADE,
  is_read BOOLEAN NOT NULL DEFAULT FALSE,
  is_seen BOOLEAN NOT NULL DEFAULT FALSE,
  payload JSONB NOT NULL DEFAULT '{}'::JSONB,
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
 
ALTER TABLE "public"."user_notifications" ENABLE ROW LEVEL SECURITY;
ALTER PUBLICATION supabase_realtime
ADD TABLE user_notifications;
 
 
CREATE policy Only_user_can_read_their_own_notification ON user_notifications AS permissive FOR
SELECT TO authenticated USING ((auth.uid () = user_id));
 
CREATE policy Only_user_can_update_their_notification ON user_notifications AS permissive FOR
UPDATE TO authenticated USING (auth.uid() = user_id);
CREATE policy Only_user_can_delete_their_notification ON user_notifications AS permissive FOR DELETE TO authenticated USING (auth.uid() = user_id);
CREATE policy Any_user_can_create_notification ON user_notifications AS permissive FOR
INSERT TO authenticated WITH CHECK (TRUE);
  • As you can see, the table has a foreign key to the user_profiles table, which means that each notification is associated with a user. This is important because it allows you to send notifications to specific users.
  • Also, the table has a payload column which is a JSONB column that can be used to store any data you want to send to the user.
  • Additionally, the table has a is_read and is_seen columns that can be used to track whether the user has read or seen the notification.
  • Finally the RLS policies ensure that users can only read, update, and delete their own notifications.

Creating a notification

To create a notification, you can use the createNotification function which is available in the src/data/user/notifications module:

src/data/user/notifications.tsx
export const createNotification = async (userId: string, payload: Json) => {
  const supabaseClient = createSupabaseUserServerActionClient();
  const { data: notification, error } = await supabaseClient
    .from('user_notifications')
    .insert({
      user_id: userId,
      payload,
    });
  if (error) throw error;
  return notification;
};

This function allows adding a notification of any payload to a user.

Typesafety

It is important to note that the payload column is of type JSONB which means that you can store any data you want in it. However, this also means that you will not get any type safety when accessing the data. For that purpose, we use zod schemas to define the payload type. It is present in src/utils/zod-schemas/notifications.ts

src/utils/zod-schemas/notifications.ts
import { z } from 'zod';
 
const invitedToOrganizationPayload = z.object({
  organizationName: z.string(),
  organizationId: z.string(),
  inviterFullName: z.string(),
  invitationId: z.string(),
  type: z.literal('invitedToOrganization'),
});
 
export const acceptedOrganizationInvitationPayload = z.object({
  userFullName: z.string(),
  organizationId: z.string(),
  type: z.literal('acceptedOrganizationInvitation'),
});
 
export const welcomeNotificationPayload = z.object({
  type: z.literal('welcome'),
});
 
export const userNotificationPayloadSchema = z.union([
  invitedToOrganizationPayload,
  acceptedOrganizationInvitationPayload,
  welcomeNotificationPayload,
]);
 
export type UserNotification = z.infer<typeof userNotificationPayloadSchema>;

As you can see, we define a UserNotification type which is a union of all the possible notification types. This allows us to get type safety when accessing the notification payload. When you want to create a new notification type you can simply add it to the union type and define the zod schema for it. For eg. if you want to create a notification type for when a user's profile was seen by another user, you can do the following:

src/utils/zod-schemas/notifications.ts
import { z } from 'zod';
 
const profileViewedNotificationPayload = z.object({
  viewerFullName: z.string(),
  viewerId: z.string(),
  type: z.literal('profileViewed'),
});
 
const invitedToOrganizationPayload = z.object({
  organizationName: z.string(),
  organizationId: z.string(),
  inviterFullName: z.string(),
  invitationId: z.string(),
  type: z.literal('invitedToOrganization'),
});
 
export const acceptedOrganizationInvitationPayload = z.object({
  userFullName: z.string(),
  organizationId: z.string(),
  type: z.literal('acceptedOrganizationInvitation'),
});
 
export const welcomeNotificationPayload = z.object({
  type: z.literal('welcome'),
});
 
export const userNotificationPayloadSchema = z.union([
  invitedToOrganizationPayload,
  acceptedOrganizationInvitationPayload,
  welcomeNotificationPayload,
  profileViewedNotificationPayload,
]);
 
export type UserNotification = z.infer<typeof userNotificationPayloadSchema>;

Parsing notification payload

We utilize a parse notification function in src/utils/parseNotification.ts, where we parse and normalize the notification payload. This is useful when we want to display the notification in the UI.

src/utils/parseNotification.ts
import {
  UserNotification,
  userNotificationPayloadSchema,
} from './zod-schemas/notifications';
 
type NormalizedNotification = {
  title: string;
  description: string;
  image: string;
  type: UserNotification['type'] | 'unknown';
} & (
  | {
      actionType: 'link';
      href: string;
    }
  | {
      actionType: 'button';
    }
);
export const parseNotification = (
  notificationPayload: unknown,
): NormalizedNotification => {
  try {
    const notification =
      userNotificationPayloadSchema.parse(notificationPayload);
    switch (notification.type) {
      case 'invitedToOrganization':
        return {
          title: 'Invitation to join organization',
          description: `You have been invited to join ${notification.organizationName}`,
          // 2 days ago
          href: `/invitations/${notification.invitationId}`,
          image: '/logos/logo-black.png',
          actionType: 'link',
          type: notification.type,
        };
      case 'acceptedOrganizationInvitation':
        return {
          title: 'Accepted invitation to join organization',
          description: `${notification.userFullName} has accepted your invitation to join your organization`,
          href: `/organization/${notification.organizationId}/settings/members`,
          image: '/logos/logo-black.png',
          actionType: 'link',
          type: notification.type,
        };
      case 'welcome':
        return {
          title: 'Welcome to the Nextbase',
          description:
            'Welcome to the Nextbase Ultimate. We are glad to see you here!',
          actionType: 'button',
          image: '/logos/logo-black.png',
          type: notification.type,
        };
      default: {
        return {
          title: 'Unknown notification type',
          description: 'Unknown notification type',
          href: '#',
          image: '/logos/logo-black.png',
          actionType: 'link',
          type: 'unknown',
        };
      }
    }
  } catch (error) {
    return {
      title: 'Unknown notification type',
      description: 'Unknown notification type',
      image: '/logos/logo-black.png',
      actionType: 'button',
      type: 'unknown',
    };
  }
};

So to add our new notification type, we can simply add a new case to the switch statement:

src/utils/parseNotification.ts
// ...
 
export const parseNotification = (notificationPayload: unknown) : NormalizedNotification =>
  try {
    const notification =
      userNotificationPayloadSchema.parse(notificationPayload);
    switch (notification.type) {
      case 'invitedToOrganization':
      // ...
      case 'acceptedOrganizationInvitation':
      // ...
      case 'welcome':
      // ...
      case 'profileViewed':
        return {
          title: 'Your profile was viewed',
          description: `${notification.viewerFullName} viewed your profile`,
          href: `/profile/${notification.viewerId}`,
          image: '/logos/logo-black.png',
          actionType: 'link',
          type: notification.type,
        };
      default: {
        // ...
      }
    }
  } catch (error) {
    // ...
  }
};

Viewing notifications

UI to mark a notification as seen and read is already built into Nextbase Ultimate.