Rizzbot AI

Released on

-

5 min read

Written by Joar Maltesson

writer
cover image

Introduction

Rizzbot AI is the largest code base I have worked on. I'm the only developer on the code base which handles AI integrations, news, login systems, rate limiting, payments and more. In this blog posts I want to share some of the solutions used in this project.

Project structure and technologies

The project is a NEXT JS fullstack application. The project uses a lot of dependencies to accomplish the desired outcome. Some of these are Drizzle ORM to handle database connection and interactions while adding type safety, Stripe to handle payments, Vercel AI tool kit to handle streaming AI data, Vercel PG & KV for data storage, React query for infinite scrolling and framer motion for some animation. For the project structure it looks something like this.

Rizz_bot
.
|____news
|____status
|___app
| |___(route_group)
| | |___chat/[id]
| | | |___page.tsx
| | |___profile
| | | |___page.tsx
| | |___store
| | | |___page.tsx
| |___api
| | |___chat/gpt
| | | |___route.ts
| | |___webhooks/stripe
| |   |___route.ts
| |___login
| | |___page.tsx
| |___signup
| | |___page.tsx
| |___globals.css
| |___layout.tsx
|___components
|___emails
|___lib
|___public
|___middleware.ts
|___"config files"

The app directory is the main entry point for the application it handles all the routes, pages, layouts, metadata and api endpoints. The _news and _status directory store markdown files that are used as a simple CMS. The other folders are pretty self explanatory, components store components used throughout the app, emails stores email templates used to notify users of different events, lib stores helper and server functions, public stores publicly served assets like images and middleware.ts handles the middleware. Each folder is organised with sub folders as needed. As an example the lib folder is broken down further into folders like auth, chat and validations to make the code base easier to navigate.

Modeling the data

If you remember from the introduction I mentioned that the site uses both a Postgres SQL and Redis KV database. The purpose of this is to separate concerns. The Postgres database handles all large persistent data with complex relationships like users, chats and messages while the KV store handles short lived data with frequent updates like rate limiting, reset/verification codes. As for the SQL schema it's pretty simple, I have 4 tables, one for users that stores their email, id username and hashed password. There is a sessions table with an id, userId that references the user it belongs to and an expiry date. For storing the chats there's two tables the chat table stores metadata about the chat like it's id, title, userId referencing the chat owner if it is public and some other data and the message table stores the actual messages and required data to associate it with the chat and render the UI.

Securing the application

The site uses email and password authentication while there are alternatives like OAUTH I like to roll my own auth from time to time. The auth is built on top of the Lucia auth library. Passwords have a minimum character limit of six and are required to include at least one uppercase, one lowercase and one numeric character to encourage stronger passwords. This results in a minimum of 62^6 possible password combinations. Assuming you use secure passwords this would take some effort to crack. To further reduce the effect of brute force attacks the login endpoint is ratelimited with a sliding window of 10 requests every 5 minutes. This results in it taking 62^6 / 2 minutes or nearly 54 000 years to test every possible password combination meeting the minimum requirement. The implementation also allows for integration of TOTP 2FA and webauthn passkeys though it is nothing that is currently implemented.

Implementing the chat functionality

Chats are created when a message is sent in an empty chat, the server then inserts a new row into the chats table and two new rows into the messages table one for the user message and one for the response. The response is also streamed back to the user in real time so the user isn't staring at a blank screen while the response is being generated. When sending a message in an existing chat the same thing happens except that no new chat is created. The message are simply added to the messages table and associated with the active chat id. chats can be shared by clicking the share button a chat in the sidebar allowing other users to interact with your chats.

Payments

As you might know, AI isn't free. So unless I want to sink money into a website that has no way of making money there needs to be some form of payment system and way of making sure users pay for what they use. I choose to go for a token based approach. You get three free messages a day. After your free messages have been used up you either have to wait or buy tokens. The payments are made through stripe through an embedded payment form on the website. To make sure successful payments always give the coins to the user and send out a receipt there is a webhook endpoint that stripe pings when a successful payment is made. If this endpoint errors and doesn't respond with 200 stripe will retry hitting the endpoint til it succeeds.

Ending

Hope you enjoyed reading this as much as enjoyed writing it. If you would want to try the website out for yourself it is live at rizzbotai.com.