Check out v4 of the Fauna CLI

v4 of the Fauna CLI is now in beta.

The new version introduces enhancements to the developer experience, including an improved authentication workflow. To get started, check out the CLI v4 quick start.

Event Streaming sample app

This reference application show how you can use Fauna Event Streaming to build real-time apps. You can use it as a starting point for your own app.

The complete source code can be found on GitHub.

Prerequisites

  • Knowledge of Javascript, React, and Next.js

  • A Fauna account

  • Knowledge of FQL

Learning Objectives

  • How to use Event Streaming

Overview of Event Streaming

Fauna’s Event Streaming feature can manage real-time data processing in your applications. You can create and subscribe to event sources for collections, indexes, or a single document.

To create an event source, write a query defining the documents to watch and convert it to an event source using set.eventSource() or set.eventsOn(). The following example creates an event source on a collection:

Product.all().eventSource()

In the example you are you subscribing to a collection called Product. When a new document is added, updated or deleted in the Product collection, the event source emits an event.

The following is another example of creating an event source using the .eventsOn() method:

Product.all().eventsOn(.price)

.eventsOn() only creates an event when changes are made to the defined fields. In the example, it is watching for changes to the price field in the Products collection.

You can also subscribe to a single document:

let product = Product.where(.name == "cups").first()
Set.single(product).eventSource()

Set up the sample application

This procedure uses v4 of the Fauna CLI, which is in beta. For the latest GA version, see v3 of the Fauna CLI.

  1. Clone the repository from GitHub:

    git clone https://github.com/fauna-labs/chat-app-streaming
  2. Install the dependencies:

    cd chat-app-streaming
    npm install
  3. Next configure Fauna database. If you haven’t already, log in to Fauna using the Fauna CLI:

    fauna login
  4. Use the Fauna CLI to create a new database:

    # Replace 'us' with your preferred Region Group:
    # 'us' (United States), 'eu' (Europe), or `global`.
    fauna database create \
      --name chat_app \
      --database us
  5. Push the schema to Fauna:

    fauna schema push \
      --database us/chat_app \
      --dir ./schema

    When prompted, accept and stage the schema.

  6. Check the status of the staged schema:

    fauna schema status \
      --database us/chat_app
  7. When the status is ready, commit the staged schema to the database:

    fauna schema commit \
      --database us/chat_app

    When prompted, agree and commit the changes. The commit applies the staged schema to the database.

  8. Open a shell session to run queries on your chat_app database:

    fauna shell \
      --database us/chat_app
  9. Create a new key for the UnAuthenticatedRole:

    Key.create({ role: 'UnAuthenticatedRole' })
    {
      id: "395112986085163081",
      coll: Key,
      ts: Time("2099-04-14T04:12:36.920Z"),
      secret: "fn...",
      role: "UnAuthenticatedRole"
    }
  10. Press Ctrl+D to exit the shell.

  11. Create a .env file in the root directory of your project and save the generated key and the Fauna endpoint:

    NEXT_PUBLIC_FAUNA_UNAUTHENTICATED_SECRET=<your-key>
    NEXT_PUBLIC_FAUNA_ENDPOINT=https://db.fauna.com
  12. Run the application:

    npm run dev
  13. Open the app in your browser at http://localhost:3000

Review the application code

Viewing rooms in real time

The application is built using Next.js. In the home page /src/app/page.js, users can create chat rooms and join existing conversations.

The code uses Event Streaming to subscribe to real-time changes to the Room collection:

// ... Rest of the component's code

const response = await client.query(fql`Room.all().eventSource()`);
const eventSource = response.data;

if (!streamRef.current) {
  streamRef.current = await client.stream(eventSource);
  for await (const event of streamRef.current) {
    switch (event.type) {
      case "start":
        console.log("Stream start", event);
        break;
      case "update":
      case "add":
        setExistingRooms(prevRooms => {
          const existingRoom = prevRooms.find(room => room.id ===
          event?.data.id);
          return existingRoom ? prevRooms : [...prevRooms,
          event?.data];
        });
        break;
      case "remove":
        console.log("Stream remove:", event);
        // Handles removal of rooms
        break;
      case "error":
        console.log("Stream error:", event);
        // Handles errors
        break;
    }
  }

  // Close the stream when the component unmounts
  return () => {
    streamRef.current.close();
  };
}

Real-Time Messaging

The core of a real-time chat application is its messaging functionality. You can find the code for chat functionality in the file:

const startMessageStream = async () => {
   try {
     const response = await streamClient.query(fql`
       let roomRef = Room.byId(${id})
       Message.byRoomRef(roomRef).eventSource()
     `);
      const eventSource = response.data;
     if (!messageStream.current) {
       messageStream.current = await streamClient.stream(eventSource)
       .on("start", event => {
         console.log("Stream start", event);
         getExistingMessages();
       })
       .on("add", event => {
         console.log("Stream add", event);
         setMessages(prevMessages => {
          const existingMessage =
            prevMessages.find(msg => msg.id === event?.data.id);
          return existingMessage ?
            prevMessages : [...prevMessages, event?.data];
         });
       })
       .on('error', event => {
         console.log("Stream error:", event);
       });
       messageStream.current.start();
     }
   } catch (error) {
     console.error("Error fetching data:", error);
   }
 };

The code snippet shows the core functionality of setting up a stream to monitor changes in the Message collection for a room with the given id.

User authentication

The sample app uses Fauna ABAC for user authentication. If you cloned the application from GitHub and ran the setup steps, you already have authentication set up. To learn more about Fauna authentication, follow the Build an end-user authentication system guide.

You can find the app’s authentication logic in src/app/authenticationform/page.js file:

const handleAuth = async (e) => {
  e.preventDefault();

  if (isLoginView) {
    try {
      const result = await client.query(fql`
        Login(${username}, ${password})
      `)

      const userInfo = {
        username: result?.data.user.username,
        id: result?.data.user.id,
        key: result?.data.cred.secret,
      };
      setCookieWithTTL("chat-loggedin",
      JSON.stringify(userInfo), 1440 * 60 * 1000);
      router.push('/');
    } catch (error) {
      console.log(error)
    }
  } else {
      try {
        const result = await client.query(fql`
          let user = User.byUsername(${username}).isEmpty()
          if(user == true) {
          Signup(${username}, ${password})
          } else {
          let message = "This username is already taken, select another"
            message
          }
        `)

        if (result.data == "This username is already taken, select another") {
          alert("This username is already taken, select another");
          setUsername('')
        } else {
          setIsLoginView(true);
          setUsername('');
          setPassword('');
          alert('Account created, please login now')
        }
      } catch (error) {
        console.log(error)
      }
    }
};

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!