The "SPOCK" Stack for Rapid Prototyping
What you're looking at is the actual side projects directory on my computer. None of these side projects have gone on to become the next Facebook yet, but I have not failed. I've just found hundreds of apps that don't work. If you're a developer entrepreneur, it's extremely important not to over engineer things. Andy Jassy once said invention requires two things: one, the ability to try lots of experiments, and two, not having to live with the collateral damage of failed experiments.
In today's video, we'll build a full stack chat app using PocketBase and Svelte, then deploy it to a Linux server on Linode for just five dollars. I call this the Spock stack, and prepare to be amazed at how quickly it can get things done. Stupidly, I made this demo public so you can actually try it out on the internet right now. This is not my first rodeo and I know you guys will spam it with the most horrific comments imaginable, but this time messages can be moderated with Community reactions. If a comment gets pooped on five times, it disappears from the UI.
The back end magic behind this application is a relatively new tool called PocketBase, which is like a free and open source Firebase written in Go that provides user authentication along with a real-time database powered by SQLite. But unlike any other base out there, it compiles everything to a single executable, and that means to self-host it all you need is one little server which costs less than your Grande soy milk macchiato. Simplicity is the ultimate sophistication, but today we'll find out if the simplicity of PocketBase can actually scale.
Deploying a $5 Linux Server on Linode
To kick things off, we're going to start things in reverse by deploying PocketBase to a Linux server in the cloud. Luckily for us, Linode has sponsored this video which means I get to make it rain with a hundred dollars in free credits for everyone watching this video. For those that don't know, Linode is one of the original cloud providers that's been around since 2003. Their platform is a perfect fit for this project because, like Svelte and PocketBase, it's famous for simplicity. Unlike big cloud providers, you can get a Linux server up and running with a few clicks with flat, predictable pricing that won't cause collateral damage later.
After you register, go to the dashboard and click the button to create a new Linode. Then choose an image with your preferred Linux distro. I'm going with Debian, but the real chads out there will use Arch by the way. Then choose a region and the size of your machine, and finally give it a password and click create. Step one is complete. We now have a Linux server running in the cloud. I told you that would be easy.
Self-Hosting PocketBase with SSH & SCP
Let's go ahead and make a note of its IP address to access it on the internet. Now in step two, let's deploy PocketBase to it. First, download and unzip PocketBase from the website. I'm also using Debian on my local machine so we'll download the Linux version. Unzip it to get this single PocketBase executable. You can run it locally by running pocketbase serve, but I want to run it in the cloud on my Linode server so I can access it from anywhere, just like a platform like Firebase.
Let's open up the terminal and use secure shell to access the Linode IP address as a root user. It'll require the password you created earlier and now we have access to the mainframe. We're currently in the root directory, so let's go ahead and create another directory called PB. And now we just need to upload the PocketBase executable to this directory. We could do it with rsync or an application like FileZilla, but I think the easiest approach is to use secure copy, which will simply copy the local file over to the remote directory we just created. Run this command from a separate terminal on your local machine. Once that's done, we can go back to the terminal on the Linode machine and run the pocketbase serve command. And to make it accessible over the Internet, we'll use the HTTP flag followed by our IP address and port 80. Congratulations, you just self-hosted your own back end that you can now access over HTTP with the IP address. It serves a REST API as well as an admin dashboard that you can access with the underscore URL.
Production-Ready PocketBase Setup
However, there's a few more things I want to show you to make it fully production ready. The first thing I would do is bring my domain over to Linode. It has a fully featured DNS manager and PocketBase uses Let's Encrypt to automatically generate an SSL certificate to serve your backend over HTTPS. All you have to do is add this extra flag to the serve command.
Now the next thing I would consider doing is mounting a volume to the PB data directory. This is where PocketBase stores all of your data. A volume is just its own file system that can be moved around to different servers on Linode. We can easily attach an extra 20 gigabytes for just two dollars. And then finally, I would recommend setting up a systemd service to automatically restart PocketBase whenever the server reboots.
Data Modeling and Securing with API Rules
And now it's time for step three, where we model the data for our chat app. Navigate to your PocketBase dashboard and create an admin user. From here we can go to the database and by default, it has a collection for users. These are the users of your application which contain fields like username, email, password, and so on.
What we need is a way to create a one-to-many relationship between users and chat messages. Create a second collection called messages and give it two fields. The first field is text for the message content itself and you can add some optional server-side validations here like min length and max length. While the second field, user, creates a relationship that says a message belongs to a user. Under the hood, PocketBase creates a foreign key for the user ID on the messages table in the SQLite database. Go ahead and click create to update the schema and now you should be able to create new records in the database.
That takes care of our data modeling, now let's make it secure. Every collection in PocketBase can have API rules. These rules determine who can access your data from a front-end application. By default, the messages collection can only be accessed by admin users. However, we want anybody to be able to view the messages, so let's go ahead and hit the unlock button for list and view. Users also need the ability to create new messages, but it's critical that they only create messages associated to their own user ID. We can enforce that rule by ensuring that the user property is equal to the request auth user ID. And just like that, we've eliminated tons of complexity that we would need in our backend code otherwise.
Save the changes, then head over to the users collection. For users, we already have a bunch of rules built in but we want to give users the ability to view each other's profiles, so let's go ahead and remove the rules for view and list to make this data public.
Svelte Setup & Reactive Auth Store
Now that we're somewhat secure, we can start building out the front end with Svelte. As you may know, I use Svelte to build the fireship.io website, but it's also been used by the PocketBase developers to build their admin and UI as well. It's a great fit for this project because it's really good at building real-time UIs thanks to its built-in reactive stores. Generate a new project with Vite, then select the Svelte option and we'll also go with TypeScript. Open the project, run npm install, then npm run dev to serve it locally.
Then in the code, delete all the boilerplate and create a new file called pocketbase in the lib directory. Whenever building a new app, I usually start by thinking about user authentication. What we'll do in this file is provide a way to listen to that current user in real time from anywhere in the application. To handle that, we'll install the PocketBase JavaScript SDK with npm. Once installed, we can then import it in this file as well as writable from svelte/store. Now to connect to our backend, all we have to do is initialize PocketBase with the URL it's hosted at, which in this case would be the Linode IP address.
From there, I'm creating a variable for currentUser that's equal to a writable store. Its default value is the PocketBase auth store model, which is equal to null when the user is not logged in, or equal to the database record of the currently logged-in user that contains fields like the user ID, username, and so on. Now the value of this model will change when the user signs in and signs out, and we can listen to those changes by registering a callback for the onChange event. When this event fires, we can update the Svelte store with the current model. That gives us a way to subscribe to the current user.
Building a Svelte Login Component
Let's put it to use by creating a login.svelte component. Inside this component, we'll log in a user with email and password. What's awesome about that store we just created is that we can subscribe to the current value anywhere in the application reactively by putting a dollar sign in front of it. For example, we might say if $currentUser, render the text signed in as $currentUser.username.
Now the question becomes, how do we sign in a user? Well, let's start by creating variables for username and password. Then create an HTML form that has two inputs for those values, and in Svelte we can easily bind the variable to the form value with the bind:value directive. Now we can access the current form value in our script. Let's create an async function called login that awaits a call to the PocketBase collection of users, then calls the authWithPassword method using the username and password as arguments. and that's all the code it takes to authenticate a user. However, we don't have a user yet, which means we'll also need another function named signup. In this function, we'll set up some initial data for a new user, like the username, password, and we could also pass a name and email address here as well, or whatever custom fields you configured in PocketBase. We can then take that data and create a new record in the users collection. Once created, we call login with that same user data. And that's all it takes to sign up.
But it'd be a good idea to wrap this in a try-catch block and render an error message if a bad username or password is used. We also need a signOut function, which is accomplished by calling authStore.clear(). Now let's go into the HTML and bind these functions to buttons, which is accomplished in Svelte with on:click. And as one final touch, I'm also going to listen to the on:submit event on the form and prevent the default action so it doesn't refresh the page when submitted.
Now let's open the app.svelte file and declare the login component there. If we view the app in the browser, we should now see this login form. And when you enter a username and password and click sign up, the UI should automatically re-render to show the current user's username.
Fetching and Displaying Chat Messages
And now we're finally ready to build the fun part: the real-time chat messages feature. Let's go ahead and create a new component called Messages. We can import the onMount and onDestroy lifecycle hooks from Svelte as well as PocketBase. Our first goal here is to fetch the most recent messages, which we can represent initially as an empty array. From there, we can fetch the messages when the component is first initialized with onMount, and this code will run once at the beginning of the component lifecycle to make a query to the messages collection. Use getList to fetch a paginated list of messages that starts on page 1 and has 50 messages per page. It also takes an object where you can do things like sort and filter the query. In our case, we want it sorted by that created field.
What's really cool though is how easy it is to join in relational data. Remember, every message has a user field that points to a user record. If we want to also include the user data on every message, we can do that with the expand option, and that means we get all the data we need without any complex SQL joins. Now the return value is a result list and we can set our messages array as the result.list.items. From there, we can go into our HTML and loop over the messages with a Svelte each block. Notice how for each message I have the message ID in parentheses; this creates a keyed loop so Svelte can keep track of all the items in the list to render them more efficiently.
For each message, I'm going to render out the message text in the database and also because we join in the user, I'm going to render out the user's username and also generate a unique avatar for each user using an awesome little service called DiceBear. Let's go ahead and try it out in the browser. I also have the PocketBase admin UI side-by-side and as you can see here, it fetches all the messages from the database. That's pretty cool, but how do we create a new message? Let's go back into the code and set up a variable for a new message. Then define a function called sendMessage. For the data, we'll need the text of the new message and also that current user's user ID. Remember, we can always get that by putting a dollar sign in front of it because Svelte is awesome. And now we just tell PocketBase to create a new message. Let's go ahead and take this function and bind it to the on:submit event on a form that also prevents the default action and binds the value of the text to that new message variable. Let's go ahead and open it up in the browser again and try to create a new message. As you can see here, it creates a new message, but we need to refresh the entire page to actually see the message, and that's not going to fly for a real-time chat app.
Implementing Real-Time Updates & Subscriptions
The final thing we'll do here is make these messages update in real time. Let's go back into the onMount callback and then we'll call pocketbase.collection('messages') and subscribe to the entire collection with the star symbol. This allows us to run a callback anytime a message is created, updated, or deleted. The action tells us what happened. In our case, we want to update the UI if a new message was created or deleted. When created, we can update the messages array with that new record. One caveat with real time though is that it won't include that expanded user record, although we can easily fetch it with a getOne query if needed. Then for the delete action, we can filter out the ID of the message that was removed.
That gives us a real-time listener, but whenever working with real time, it's a good practice to unsubscribe when the data is no longer needed because otherwise you could end up with memory leaks and unnecessary reads on the database. Create a variable to represent a function named unsubscribe. We obtain this function from PocketBase when making the call to subscribe. Once we have that function as state, we can then call it on the onDestroy lifecycle hook to end the subscription when the user logs out or if they navigate to a different page.
Final Deployment and Vertical Scaling
Let's open up the browser and try it out. You should now have a chat application that updates the messages feed in real time. And now we can run npm build to build our site for production and deploy it somewhere on the internet. In fact, because we have a single page application, we could actually do this on Linode by uploading the generated files to a storage bucket then configuring it as a static website host.
The final thing you want to know is that as your website grows, PocketBase will need to scale vertically with more CPU and RAM to handle more and more traffic. This can be easily accomplished by simply resizing your Linode. A bigger server can handle more traffic. It can easily handle tens of thousands of concurrent users and should be more than adequate for the vast majority of projects out there. This allows you to fail over and over again as a developer without a bunch of wasted time and money as collateral damage. Thanks for watching and I will see you in the next one.