This guide will help you get started with Towns Protocol React SDK. You’ll learn how to:
- Install the React SDK
- Set up the necessary providers
- Connect to Towns
- Create a space
- Send your first message
Installation
You can start a new Towns project in two ways:
Using create-towns-protocol-app (Recommended)
The easiest way to get started is using create-towns-protocol-app
and follow the instructions in the terminal. This will set up a new React project with all the necessary dependencies and configurations.
# npm
npm create towns-protocol-app my-app
# yarn
yarn create towns-protocol-app my-app
# pnpm
pnpm create towns-protocol-app my-app
By default, this will create a new app using the Towns Playground template - a full-featured example application that demonstrates Towns’s Protocol capabilities.
Manual Installation
If you prefer to add the React SDK to an existing project, install the required dependencies:
yarn add @towns-protocol/react-sdk @towns-protocol/sdk vite-plugin-node-polyfills
Then, add the vite-plugin-node-polyfills
to your vite.config.ts
:
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
// ...rest of your config
plugins: [
// ...rest of your plugins
nodePolyfills(),
react(),
],
})
Setting Up Providers
Towns requires a few providers to be set up at the root of your application:
WagmiProvider
- For Web3 wallet connection (recommended)
TownsSyncProvider
- For interacting with Towns Protocol
Here’s how to set them up:
import { TownsSyncProvider } from "@towns-protocol/react-sdk";
import { base, baseSepolia } from 'viem/chains'
import { WagmiConfig, createConfig } from "wagmi";
import { createPublicClient, http } from 'viem'
// Configure Wagmi
export const wagmiConfig = createConfig({
autoConnect: true,
publicClient: createPublicClient({
chain: [base, baseSepolia],
transport: http()
}),
})
function App() {
return (
<WagmiConfig config={wagmiConfig}>
<TownsSyncProvider>
{/* Your app content */}
</TownsSyncProvider>
</WagmiConfig>
);
}
If you need any assistance with setting up Wagmi, check out the Wagmi Getting Started Guide.
Connecting to Towns Protocol
You can connect to Towns using either a Web3 wallet (recommended) or a bearer token.
You can use the following networks:
omega
- The Towns mainnet, uses Base as the EVM chain
gamma
- The Towns testnet, uses Base Sepolia as the EVM chain
We recommend using gamma
as a starting point.
Using a Web3 Wallet
import { useAgentConnection } from "@towns-protocol/react-sdk";
import { makeRiverConfig } from "@towns-protocol/sdk";
import { getEthersSigner } from "./utils/viem-to-ethers";
import { wagmiConfig } from "./config/wagmi";
const riverConfig = makeRiverConfig("gamma");
function ConnectButton() {
const { connect, isAgentConnecting, isAgentConnected } = useAgentConnection();
return (
<button
onClick={async () => {
const signer = await getEthersSigner(wagmiConfig);
connect(signer, { riverConfig });
}}
>
{isAgentConnecting ? "Connecting..." : "Connect"}
{isAgentConnected && "Connected ✅"}
</button>
);
}
You’ll need to use getEthersSigner
to get the signer from viem wallet client.
Towns SDK uses ethers under the hood, so you’ll need to convert the viem wallet client to an ethers signer.
You can get the getEthersSigner
function from Wagmi docs.
Using a Bearer Token
You can connect to Towns using a pre-existing identity.
This allows you to read and send messages, but you won’t be able to create spaces or channels (on-chain actions).
If you have a Towns account, you can get your bearer token from there. Type /bearer-token
in any conversation to get your token.
import { useAgentConnection } from "@towns-protocol/react-sdk";
import { makeRiverConfig } from "@towns-protocol/sdk";
const riverConfig = makeRiverConfig("gamma");
function ConnectButton() {
const { connectUsingBearerToken, isAgentConnecting, isAgentConnected } = useAgentConnection();
const [bearerToken, setBearerToken] = useState('');
return (
<div>
<input
value={bearerToken}
onChange={(e) => setBearerToken(e.target.value)}
placeholder="Enter bearer token"
/>
<button
onClick={() => connectUsingBearerToken(bearerToken, { riverConfig })}
>
{isAgentConnecting ? "Connecting..." : "Connect"}
{isAgentConnected && "Connected ✅"}
</button>
</div>
);
}
Creating a Space
Once connected, you can start interacting with Towns. Here’s how to create a space:
You can only create a space with a Web3 wallet. If you’re using a bearer token, you can’t create spaces.
import { useCreateSpace } from "@towns-protocol/react-sdk";
import { getEthersSigner } from "@/utils/viem-to-ethers";
import { wagmiConfig } from "@/config/wagmi";
function CreateSpace({ onCreateSpace }: { onCreateSpace: (spaceId: string) => void }) {
const { createSpace, isPending } = useCreateSpace();
const [spaceName, setSpaceName] = useState('');
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const signer = await getEthersSigner(wagmiConfig);
const { spaceId } = await createSpace({ spaceName }, signer);
// Let a parent component handle the spaceId, keep reading the guide!
onCreateSpace(spaceId);
// Reset form
setSpaceName('');
}}
>
<div>
<input
value={spaceName}
onChange={(e) => setSpaceName(e.target.value)}
placeholder="Space name"
required
/>
</div>
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create Space"}
</button>
</form>
);
}
Sending Your First Message
With the space created, we can send a message to the #general
channel.
Here’s how to create a form
import {
useChannel,
useSendMessage,
useSpace,
useTimeline,
useUserSpaces,
} from '@towns-protocol/react-sdk'
import { RiverTimelineEvent } from '@towns-protocol/sdk'
import { useState } from 'react'
// Form to send a message to a channel
function ChatInput({ spaceId, channelId }: { spaceId: string; channelId: string }) {
const { sendMessage, isPending } = useSendMessage(channelId)
const { data: channel } = useChannel(spaceId, channelId)
const [message, setMessage] = useState('')
return (
<form
onSubmit={(e) => {
e.preventDefault()
sendMessage(message)
setMessage('')
}}
>
<input
value={message}
placeholder={`Type a message in ${channel?.metadata?.name || 'unnamed channel'}`}
onChange={(e) => setMessage(e.target.value)}
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Send'}
</button>
</form>
)
}
// Display messages from a channel
function ChatMessages({ channelId }: { channelId: string }) {
const { data: events } = useTimeline(channelId)
return (
<div>
{/* Filter out non-message events */}
{events
?.filter((event) => event.content?.kind === RiverTimelineEvent.ChannelMessage)
.map((message) => (
<p key={message.eventId}>
{message.content?.kind === RiverTimelineEvent.ChannelMessage
? message.content?.body
: ''}
</p>
))}
</div>
)
}
// Chat component that renders the chat input and messages, given a spaceId
function Chat({ spaceId }: { spaceId: string }) {
const { data: space } = useSpace(spaceId)
const channelId = space?.channelIds?.[0] // Spaces have a default channel called `general`
return (
<>
{channelId && <ChatInput spaceId={spaceId} channelId={channelId} />}
{channelId && <ChatMessages channelId={channelId} />}
</>
)
}
// Renders the name of a space
const SpaceName = ({ spaceId }: { spaceId: string }) => {
const { data: space } = useSpace(spaceId)
return <>{space?.metadata?.name}</>
}
// Chat app that renders the create space and chat components
export const ChatApp = () => {
// Get the list of spaces we're a member of
const { spaceIds } = useUserSpaces()
const [spaceId, setSpaceId] = useState('')
return (
<div>
{/* Show a list of spaces we're a member of, click to select one */}
{spaceIds.map((spaceId) => (
<button type="button" key={spaceId} onClick={() => setSpaceId(spaceId)}>
<SpaceName spaceId={spaceId} />
</button>
))}
{/* Show the create space form if we don't have a space */}
{!spaceId && <CreateSpace onCreateSpace={(spaceId) => setSpaceId(spaceId)} />}
{/* Render the chat component with the current selected space */}
{spaceId && (
<h1>
Chatting in <SpaceName spaceId={spaceId} />
</h1>
)}
{spaceId && <Chat spaceId={spaceId} />}
</div>
)
}
Add the ChatApp
component to your app and you’re ready to start chatting!
Next Steps
Now that you have the basics set up, you can:
Check out our Playground template for a full-featured example application.