Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/astrxnomo/tourify/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Tourify has a two-part notification system:

Push notifications

Delivered in real-time via the Expo Push Service. The mobile app registers its device token with the server; the server uses that token to dispatch pushes.

In-app notification center

A persistent list of notifications stored in the database. Users can read and mark them as read from inside the app using the notifications API.
All notification endpoints require authentication.

Push token registration

POST /api/push-token

After login, the mobile app obtains an Expo push token from the device and registers it with the server. The token is stored on the users table in the push_token column so the server knows where to deliver future pushes for that user. Required body field
token
string
required
A valid Expo push token in the form ExponentPushToken[xxxxxxxxxxxxxxxxxxxx].
curl -s -X POST http://localhost:8000/api/push-token \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 15|xYzAbCdEfGhIjKlMnOpQrStUvWx0987654321" \
  -d '{
    "token": "ExponentPushToken[FqOJBU-tKnPbDCIl1fGvFW]"
  }'

How the frontend registers push tokens

The usePushNotifications hook handles the full permission request and token registration lifecycle. It runs automatically when the app mounts after login.
hooks/usePushNotifications.js
import * as Notifications from "expo-notifications";
import { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import { post } from "../services/post";

// Configure how incoming pushes are handled while the app is foregrounded
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export function usePushNotifications() {
  const [expoPushToken, setExpoPushToken] = useState(null);
  const [notification, setNotification] = useState(null);

  const registerForPushNotifications = useCallback(async () => {
    // Android requires a notification channel to be configured
    if (Platform.OS === "android") {
      await Notifications.setNotificationChannelAsync("default", {
        name: "default",
        importance: Notifications.AndroidImportance.MAX,
      });
    }

    // Check existing permission status
    const { status: existingStatus } =
      await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;

    // Request permission if not yet granted
    if (existingStatus !== "granted") {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }

    if (finalStatus !== "granted") return; // user denied — skip registration

    // Retrieve the Expo push token for this device
    const tokenData = await Notifications.getExpoPushTokenAsync();
    const token = tokenData.data;
    setExpoPushToken(token);

    try {
      // Register the token with the Tourify backend
      await post("/push-token", { token });
    } catch {
      // Silently fail — push token registration is not critical
    }
  }, []);

  useEffect(() => {
    registerForPushNotifications();

    // Listen for notifications received while the app is open
    const notifSubscription = Notifications.addNotificationReceivedListener(
      (notif) => setNotification(notif),
    );

    return () => notifSubscription.remove();
  }, [registerForPushNotifications]);

  return { expoPushToken, notification };
}
Key steps the hook performs:
1

Configure Android channel

On Android, creates a "default" notification channel with MAX importance so notifications are shown as heads-up banners.
2

Request permission

Calls Notifications.requestPermissionsAsync() if the permission is not already "granted". If the user denies, the hook exits early without registering.
3

Obtain push token

Calls Notifications.getExpoPushTokenAsync() to get the unique Expo device token (format: ExponentPushToken[...]).
4

Register with the server

POSTs the token to POST /api/push-token. Failures are silently swallowed so a push-token issue never breaks the login flow.
5

Listen for foreground notifications

Attaches a addNotificationReceivedListener so the app can respond to pushes received while the app is open (e.g. update the badge count).
An admin with access to the Tourify admin panel can broadcast a push notification to all registered users at once. The server iterates over all push_token values stored in the users table and sends the message via the Expo Push API.

In-app notification center

GET /api/notifications

Returns all notifications for the authenticated user, ordered by created_at descending (newest first).
id
integer
The notification’s unique ID. Used in the mark-as-read endpoints.
user_id
integer
The ID of the user this notification belongs to.
title
string
Short notification title (e.g. "Nuevo evento en Barcelona").
message
string
Full notification body text.
is_read
boolean
Whether the user has marked this notification as read. Starts as false.
notifiable_id
integer | null
The ID of the related entity (e.g. an event ID), if applicable.
notifiable_type
string | null
The type of the related entity (e.g. "event"), if applicable.
curl -s http://localhost:8000/api/notifications \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 15|xYzAbCdEfGhIjKlMnOpQrStUvWx0987654321"

PATCH /api/notifications//read

Mark a single notification as read (is_read: true). Returns 403 if the notification does not belong to the authenticated user.
curl -s -X PATCH http://localhost:8000/api/notifications/12/read \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 15|xYzAbCdEfGhIjKlMnOpQrStUvWx0987654321"

PATCH /api/notifications/read-all

Mark all unread notifications for the authenticated user as read in a single request. Only notifications where is_read is currently false are updated.
curl -s -X PATCH http://localhost:8000/api/notifications/read-all \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 15|xYzAbCdEfGhIjKlMnOpQrStUvWx0987654321"
Call PATCH /api/notifications/read-all when the user opens the notification center screen to clear the badge count in one shot, rather than patching each notification individually.

Frontend integration

The useNotifications hook loads the notification list, exposes an unreadCount derived value, and provides markAsRead / markAllAsRead helpers that optimistically update local state without requiring a full reload.
hooks/useNotifications.js
import { useCallback, useEffect, useState } from "react";
import { get, patch } from "../services/post";

export function useNotifications() {
  const [notifications, setNotifications] = useState([]);
  const [loading, setLoading] = useState(true);

  const load = useCallback(async () => {
    setLoading(true);
    try {
      setNotifications(await get("/notifications"));
    } catch {
      setNotifications([]);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { load(); }, [load]);

  // Optimistically marks a single notification as read in local state
  const markAsRead = useCallback(async (id) => {
    await patch(`/notifications/${id}/read`);
    setNotifications((prev) =>
      prev.map((n) => (n.id === id ? { ...n, is_read: true } : n)),
    );
  }, []);

  // Marks all notifications as read in one request
  const markAllAsRead = useCallback(async () => {
    await patch("/notifications/read-all");
    setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })));
  }, []);

  // Derived count — used to display a badge on the notifications tab icon
  const unreadCount = notifications.filter((n) => !n.is_read).length;

  return {
    notifications,
    loading,
    refresh: load,
    markAsRead,
    markAllAsRead,
    unreadCount,
  };
}
Usage in a screen:
NotificationsScreen.jsx (excerpt)
const {
  notifications,
  loading,
  markAsRead,
  markAllAsRead,
  unreadCount,
} = useNotifications();

// Display badge count on the tab bar icon
<Tab.Screen
  name="Notifications"
  options={{
    tabBarBadge: unreadCount > 0 ? unreadCount : undefined,
  }}
/>

Endpoint reference

MethodPathAuthDescription
POST/api/push-token✅ RequiredRegister or update the device’s Expo push token
GET/api/notifications✅ RequiredList all notifications for the user (newest first)
PATCH/api/notifications/{notification}/read✅ RequiredMark one notification as read
PATCH/api/notifications/read-all✅ RequiredMark all unread notifications as read

Build docs developers (and LLMs) love