Get started with the React UI Kit + Next.js

This getting started guide is a fully functional web application with all the available Weavy React components integrated. I addition to the React components, we will also show you how you handle authentication, syncing user data, setup a webhook to listen for notifications and other useful things. The main goal when we created this guide was to have a near real life example how Weavy actually is integrated into a web application.

See also: React UI kit reference

Why Next.js?

Next.js is a framework for building and creating web applications. It uses React to build the UI which is a perfect fit for the Weavy React UI Kit. We want to show you how Weavy is integrated in a full-stack web application in contrast to a simpler create-react-app example.

Getting the code

  1. Start by cloning the repo at https://github.com/weavy-labs/weavy-react-nextjs.

  2. Run npm install in the root folder.

  3. Register a Weavy account. This will give you a Weavy environment to play with.

  4. Sign in to your account and create a new API key for the environment. You'll need it later.

  5. Create a .env file in the root folder and add the following:

    DATABASE_URL="file:./dev.db"
    NEXTAUTH_SECRET=""
    WEAVY_URL=""
    WEAVY_APIKEY=""
    
    • NEXTAUTH_SECRET is a secret string of your choice needed by NextAuth.
    • WEAVY_URL is the url to the environment you created in step 3.
    • WEAVY_APIKEY is the api key generated in step 4.
  6. Run npx prisma generate in the root folder. Don't worry, we come back to what this is later.

  7. Run npm run dev to start the site.

The application

So, what is this Acme website? You should think of it as the application where you want to add the functionality that Weavy offers. This could be your product, intranet or whatever web application you are building. We created the Acme website to be able to add Weavy to a "real" application. Of course, it's still a very basic example, but the important stuff here is what actually happens when a user signs in to the website and how the Weavy components are initialized and displayed.

Building blocks

These are the building blocks of the website and where Weavy is integrated in some way. Either as one of the ready-to-use React components from the React UI kit, or as a simple api request to the Weavy api. We want to show both.

Building block Acme website Weavy
Login page Authenticates a user agains the Acme local db (sqlite). After login to the Acme website, the user data is synced to Weavy
Top navigation Display notifications and switch light/dark mode Notifications from Weavy. A global Weavy Messenger component
Users list Display all the users in the Acme website db.
Edit the user.
After updating an Acme user, the user data is synced to Weavy
Menu/Pages Each of the pages under Weavy contains a Weavy React UI kit component which displays each of the Weavy apps available; Chat, Posts and Files.
Examples Example to make a request to the Weavy api
Websocket A websocket connection from the Next.js server to the frontend.
Used to send a notification to the frontend when an
incoming webhook notificatin is delivered from the Weavy environment
Prisma An ORM to handle the data access from the Acme sqlite db.

Users in the Acme website and Weavy

One concept that is important to understand is how the users in the host application, in this case the Acme website, and Weavy correlate. The users that you have in your application are managed by your application. But Weavy also need an user account for each host application user in order to be able to identify which user is performing any interaction with a component or the api. This is accomplished by supplying a Bearer token for each request.

When you are using the React UI kit, all of this is taken care of in the WeavyProvider component and the WeavyClient that you set to the WeavyProvider. What you need to supply to the WeavyClient is a function returning a valid Bearer token for the host application user. You'll learn more about this in the Authentication section below.

Authentication

Let's begin with some code to illustrate the authentication process. This code snippets are taken from the WeavyWrapper.tsx file located at \src\components\.

import { WeavyClient, WeavyProvider } from '@weavy/uikit-react';

...

const getToken = useCallback(async (refresh: boolean) => {  
    var response = await fetch(`/api/token?refresh=${refresh}`);
    var json = await response.json();
    return json.access_token;
}, []);

...

 let client = new WeavyClient({ url: process.env.WEAVY_URL, tokenFactory: getToken });
...

<WeavyProvider client={client}>        
    {children}
</WeavyProvider>

So what's going on here. First, we have defined a getToken function that's making a request to the Next.js server side api to get a valid Bearer token. Remember that the request for a new Weavy token should always be made server-to-server. Never from your frontend UI! The function is set as the tokenFactory when we we create a new WeavyClient. The client is then set on the WeavyProvider component.

The api endpoint on the Next.js server looks like this:

let _tokens: Token[] = [];

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    const refresh = req.query.refresh;
    const session = await getSession({ req })
  
    if (session.user) {
        const userId = session.user.id;      

        if ((!refresh || refresh === "false") && _tokens.find((t) => t.userId === userId)) {
        res.json({ access_token: _tokens.find((t) => t.userId === userId)?.access_token });
        } else {
        let response = await fetch(`${process.env.WEAVY_URL}/api/users/${getUserName(userId)}/tokens`, {
            method: 'POST',
            headers: {
            'content-type': 'application/json',
            'Authorization': `Bearer ${process.env.WEAVY_APIKEY}`
            },
            body: JSON.stringify({ expires_in: 3600 })
        });

        if (response.ok) {          
            let data = await response.json();
            _tokens = [..._tokens.filter((t) => t.userId !== userId), { userId: userId, access_token: data.access_token }];
            res.status(200).json({ access_token: data.access_token })
        } else {
            res.json({ message: "Could not get access token from server!" })
        }
        }
    } else {
        res.json({ message: "Could not get access token from server!" })
    }

}

The code may seem a lot, so let's break it down.

const refresh = req.query.refresh;

All React components in the Weavy UI kit will send true/false to the tokenFactory. This indicates that a request to the Weavy api from a component failed and a new token needs to be created. This will happen when a Bearer token is exired or revoked. Checking if the token need a refresh is also convenient for you as you don't need to make a request to the Weavy environment each time asking for a new token. In the example above, we have a _tokens array containing all the users tokens (in memory only). If the refresh parameter is false, we just get it from the array, otherwise, ask the Weavy environment for a new one.

let response = await fetch(`${process.env.WEAVY_URL}/api/users/${getUserName(userId)}/tokens`, {
    method: 'POST',
    headers: {
    'content-type': 'application/json',
    'Authorization': `Bearer ${process.env.WEAVY_APIKEY}`
    },
    body: JSON.stringify({ expires_in: 3600 })
});

If we need to get a new token, a request is made to the Weavy environment api endpoint /api/users/[unique id]/tokens. Note that the Weavy API key is supplied as the Bearer token. The unique id is the important thing here. This is unique id for the signed in user that Weavy uses when creating the user. This could be the user's username, id or whatever. In the Acme website, we have a getUsername helper function that always returns the user's unique id as acmeuser-[user id]. So, for example acmeuser-1.

export const getUserName = (userId: string | number | undefined) => {    
    return `acmeuser-${userId}`;
}

If the user does not exist in Weavy, the user is created. You can supply more info about the user, such as name, email etc. In the Acme website application, we don't need to do this when getting a token. Instead, we make sure the user is always synced with the correct data when signing in and updating a user. More on this in the Sync Acme user data to Weavy section below.

Sync Acme user data to Weavy

Now that you have learned how you can authenticate a user and get a token, let's check out how we can keep the Acme user's data in sync with the Weavy data. After all, when the user interacts with a Weavy component app, for example the Posts app, we want to make sure the current user profile info is displayed correctly.

In the Acme website application, we decided to do this sync in three different places. When the user signs in the the website, when the user updates the profile and when a user is edited/updated from the users list.

After login

The Acme website uses NextAuth to handle the authentication process. The NextAuth solution has a required api endpoint located at pages/api/auth/[...nextauth].ts. In the configuration, there is a authenticate function that gets called when a user logs in to the Acme website. When the authentication is completed, we can use the current user object to sync the user data to Weavy.

// sync user data to Weavy
await syncUser({id: user.id, name: user.name, email: user.email})

The syncUser() function is located at lib/weavy.ts which has some shared utility functions.

export const syncUser = async ({ id, name, email }: UserProps) => {
    let response = await fetch(`${process.env.WEAVY_URL}/api/users/${getUserName(id)}`, {
        method: 'PUT',
        headers: {
            'content-type': 'application/json',
            'Authorization': `Bearer ${process.env.WEAVY_APIKEY}`
        },
        body: JSON.stringify({ name: name, email: email, directory: "ACME" })
    });    
    return response
}

To sync the user's data, we can make a request to an endpoint in the Weavy api, /api/users/[unique user id]. We supply the user data we want to update in the body. In this case we want to update the name, email and directory. The directory is optional, but in this case we want to add all the Acme users into a director called ACME. You can group users in Weavy by adding them to a specific directory.

If the user does not exist, the user is created in Weavy. So the first time an Acme user logs in to the Acme web application, the user will also be created in Weavy.

We use the same syncUser() function when a user updates its profile and when a user is updated from the Users list. So we make sure the correct user info always is in sync with Weavy.

Adding apps

Now it's time to add som Weavy component apps to the Acme website. You can check them out under the Weavy section in the left hand side menu. All of the Chat, Feed and Files pages contains a Weavy component app added from the React UI Kit.

These apps are so called Contextual apps. These apps is meant to belong to a specific context in your application and requires you to specify a unique id when you add them. This could for example be for a specific product page, a user, a project and so on. The unique id is something you decide what it's going to be. Let's say you you are on a project page for the project My project. This page has a unique identifier in you application called project-1. The unique id for a Chat could them for example be chat-project-1.

Before you can display a contextual app, the app must already be created in Weavy. In addition to that, the user in the host application, in this case the Acme website, must also me a member of the Weavy app. The Weavy api exposes an endpoint to handle all of this:

POST /api/apps/init

In the lib/weavy.ts file, we have created a utility function that the Acme application uses when a Weavy app should be initialized.

export const initApp = async ({ uid, name, type, userId }: AppProps) => {
    let response = await fetch(`${process.env.WEAVY_URL}/api/apps/init`, {
        method: 'POST',
        headers: {
            'content-type': 'application/json',
            'Authorization': `Bearer ${process.env.WEAVY_APIKEY}`
        },
        body: JSON.stringify({ app: { uid: uid, name: name, type: type }, user: { uid: getUserName(userId) } })
    });

    return response
}

In the body, we specify the app we want to initialize, and the user we want to add to the app as a member.

Data Type Description
app
uid string The unique identifier for the app, for example chat-project-1
name string A title for the app
type string The type of Weavy app to initialize. One of chat, posts or files
user
uid string The unique identifier of the user to add as a member

The /api/apps/init endpoint will create the app if it doesn't exist, otherwise it will return the existing one. If the user isn't already a member of the app, the user will be added.

You can see this in action on the pages/weavy/acme-chat.tsx component. In the getServerSideProps which is run on the NextJS server before rendering the page, we make a call to the initApp() helper function.

export const getServerSideProps: GetServerSideProps = async (context) => {
  const session = await getSession({ req: context.req })
  const url = context.resolvedUrl;
  const uid = url.substring(url.lastIndexOf('/') + 1);

  if (session && uid) {
    const response = await initApp({ uid: uid, name: "Chat", type: "chat", userId: session?.user.id });
    let json = await response.json();

    return {
      props: { uid: json.uid, title: json.display_name }, // will be passed to the page component as props
    }
  }

  return { props: {} }
}

First of all we get the current signed in user in the Acme website. We need the user to add as a member to the app we want to create or return. Then we take the last part of the NextJS url to use as the unique identifier for the Weavy app. In this case it will be acme-chat.

We pass this along to the initApp() function along with a name and a type. In the response, we get the app json data returned from Weavy. The data is returned as props to the React component.

export default function Chat({ uid, title }: Props) {

  return (
    <>
      <Head>
        <title>Users</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="contextual-app d-flex">        
          <ChatComponent uid={uid} />        
      </div>
    </>
  )
}

The uid is the important thing here. This is what we set on the React UI kit Chat component to load and display the correct Weavy app.

If you set an uid to the Files, Chat or Posts app components that doesn't exist in Weavy, you will get a 404 error when trying to display the app.

The Weavy Messenger

The Messenger React UI kit component is a little bit different than the Chat, Posts and Files components. The Messenger is not a contextual app. You can think of it more like a global messenger where users in a directory can create private or room conversations. The Messenger component is available in the Acme website by clicking on the Messenger icon in the top right corner in the top navigation.

The Messenger component is always available in Weavy and is not needed to be created before hand. If you take a look at the src/components/Messenger.tsx component, you can see that no special initialization is done.

const MessengerComponent = dynamic(() => import('../components/CustomWeavyMessenger'), {
    loading: () => <span></span>,
    ssr: false
})

type Props = {
    isOpen: boolean
}

const Messenger = ({ isOpen }: Props) => {
    return (
        <div className={"offcanvas-end-custom settings-panel border-0 " + (isOpen ? "show" : "")} id="messenger">
            <MessengerComponent />
        </div>
    )
}

Webhooks and notications

The webhooks in Weavy allow you to build integrations that subscribe to certain events happening in the Weavy environment. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL. You can for example use webhooks to synchronize user and profile information between systems, create reports, send notifications and more.

In the Acme website, we are going to use the webhooks to show a list of notifications to the user when something relevant happens in the Weavy component apps. For example when someone post something in the feed or uploads a new file in the Files app.

Setup

First of all we need to create the desired webhook in Weavy. You can easily do this by making a request to the Weavy api.

curl -H "Authorization: Bearer {WEAVY_APIKEY}" -H "Content-Type: application/json" {WEAVY_URL}/api/webhooks -d "{'payload_url': 'http://localhost:3000/api/webhooks', 'triggers': ['notifications']}"

This will create a webhook that will post to the endpoint http://localhost:3000/api/webhooks (this is an endpoint in the host application, in this case the Acme website that should receive the webhook) whenever a notification in Weavy is created, updated or marked as read/unread.

export default async function handler(req: NextApiRequest, res: NextApiResponseWithSocket) {

  const { action, actor, notification } = req.body;

  switch (action) {
    case "notification_created":
      res.socket.server.io?.to(notification.user.uid).emit(action);
      break;
    case "notification_updated":
      res.socket.server.io?.to(actor.uid).emit(action);
      break;
  }


  res.end()
}

In the endpoint above, we receive the webhook from Weavy and push it to the frontend using a websocket. The component src/components/Notifications.tsx receives the websocket message and updates the notification list.

useEffect(() => {
    if (socket) {
        socket.on('notification_created', msg => {                
            // notification from socket                
            getNotifications(false);
        })

        socket.on('notification_updated', msg => {                
            // notification from socket                
            getNotifications(false);
        })
    }
}, [socket])

Using the Web API

In Examples/Message API we show how you can use the Web API to post a chat message. When making a request to the API, you can either use an API key or an access token as the Bearer token. The API key is used when you want to make the request as the System user and the access token is when you want to make the request as an authenticated user.

// post a message to Weavy
let response =await fetch(`${process.env.WEAVY_URL}/api/apps/${id}/messages`, {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${process.env.WEAVY_APIKEY}`,
        'Content-Type': 'application/json',
    },    
    body: JSON.stringify({
        text: text
    })
});
Weavy Docs