The Tech Behind Klart.co and Pixels

The Tech Behind Klart.co and Pixels

After writing about building Pixels some people have asked me about my technology stack for building Klart.co - A bookmarking tool for designers and Pixels - A collection of kick-ass designs. I love to answer questions about this but squishing them down into 140 characters on Twitter is a bit hard. So I decided to write a blog post about it. Here it goes 🤗.

Preface

This blog post will be a technical overview and will include very little code. It will also include a some abbreviations which you may or may not have to look up. I'm sorry about that 🙈. If you're looking for implementation details I'd love to answer your questions on Twitter or email.

Goals

To be able to make choices you have to know what your goals are. I wanted to maximize four variables: simplicity, performance, cost and privacy. We can model it as a function f(s, p, c, p) which we want to maximize. I want to mention that I like to learn and build stuff myself so this is not for everyone. It's not optimized for time.

Frontend

The frontend for Klart.co started out with Pug templates rendered on a Node/Express server. I had all styles in a single css file called styles.css. I also had a Javascript bundle for each page. Content would be rendered on server and data would be handled with JSON endpoints. For example: I would render the /snaps page with navigation and all but I would fetch all data from the /api/users/current/snaps endpoint in JSON. I could have rendered the first snaps on the server but I figured it's easier to just have one source for the data.

Today the actual app (after you login) is built with React. I started out with just the /snaps page in React since it had most state changes and interactions. But later, when other views got more complex I decided to write them in React and today the app is a single entry-point SPA. I'm not saying this is a silver bullet by any means. But for me, it was a lot easier to handle the different states with React.

Frontend build process

I'd love to be able to use something as create-react-app for the frontend. However, Klart is what's commonly called a multi-page app (has multiple entry-points). So instead of using the eject feature of create-react-app I decided it's better that I configure React and Webpack myself.

The configuration is nothing fancy. It's just one entry-point for each page/app. I also use ExtractTextPlugin to extract css into their own files and AssetsPlugin to append hashes to all files for cache busting (I only do this in production). I use a helper function that takes a filename without a hash as an argument and reads the asset manifest created by AssetsPlugin to find the corresponding file with hash included.

Code style

To keep everything nice and tidy I use Eslint with Airbnb's recommended presets + some minor adjustments where I disagree 🤓. I also run everything through Prettier which is an awesome tool that formats all your code. Kind of like Go fmt for Javascript.

Backend

For performance and simplicity I decided that Node would be a good fit for the backend. Since it allows me to focus on a single language, and in case I need to hire I can focus on hiring developers with Javascript knowledge so I can get them up to speed quickly. I know good developers can learn languages quickly. But it removes a lot of friction if you're already fluent in the language. Another good thing with Node is that it's actually pretty fast.

The backend consists of a couple of folders:

  • config/ Application wide configuration such as whitelisted domains.
  • controllers/ One controller per route group. For example, users.js maps to /api/users.
  • models/ Database models.
  • middleware/ Custom middleware for Express such as authentication and authorization.
  • helpers/ Helper functions. For example getting a static file with hash included.
  • scripts/ This is where I put migrations etc.
  • views/ Pug templates.

Worth mentioning is that I use the Router Express class for all controllers. I also group controllers to add common middleware. For example, I have an api.js controller which mounts all API controllers and has some error handling middleware to return JSON responses with appropriate status codes.

Testing

I use Mocha and Chai to test endpoints and database models. It does the job and their names make you happy ☕️. I don't use any CI/CD but will look into Gitlab's own solution soon.

Databases

I use MongoDB as database together with the Mongoose ORM. You could probably use plain Mongo but Mongoose helps a lot with validation, schema structure and population (which is pretty awesome). Population basically means that if you have a model Snap(_id, image_url, _user) and User(_id, name) you can tell Mongoose that you want to populate _user and it will make an additional query to embed your user object in the Snap object. Pretty neat.

So far MongoDB and Mongoose has been excellent. I'm working on a plan for teams as well as some collaboration features which will add some more logic. We'll see how happy I am after that 😉.

Realtime updates

The data on Klart is kept in sync across all your devices in real-time. I use SocketIO and Redis to keep track of sessions and sockets for each user. Whenever an update takes place, I push it out on the sockets that needs it.

Image serving

I got a lot of images so one of my biggest concerns is how to serve them. I've looked at using Amazon S3 and short-time tokens for requests directly to Amazon. This was kind of a no-go for me since I want to use explicit access control on all requests and not relying on obscurity such as a time frame. I could proxy the requests through my own server though. My third option was to store the images myself and serve them straight from disk.

At this point Digital Ocean was beta testing block storage so I figured that storing the images myself would be a good option in terms of my goals and I could always migrate to block storage later on.

Authentication

I use an awesome library called Passport for authentication. I store the sessions in a Redis database running on the same server as the application. I also have some custom middleware to handle authorization for users with different plans etc.

Payments

I use Stripe as a payment processor as well as subscription management. Besides a bunch of attempted payments being declined on launch, Stripe has worked above expectations and their API is extremely well documented. When a user invoice is paid, Stripe sends me a webhook request which I can verify and update the corresponding users plan in my database. If no webhook request comes, the user hasn't made payment and will eventually not have an active plan. Easy peasy.

Servers

Everything is deployed on a Digital Ocean droplet. The UI is great and I've been very happy with support and uptime so far. It feels like a breath of fresh air compared to Amazon's products (which is also awesome btw). I use IP tables to setup rules for what kind of network traffic is allowed.

Additional layers

I use PM2 to handle my Node process and load confiurations using dotenv and environment variables. I also have Nginx configured as a reverse proxy in front of my Node instances. Finally, I use CloudFlare in front of everything to speed things up a bit.

Deployment

To handle deployments I've configured a bare GIT repository on the server. I've added this remote and named it live on my development machine. To push anything to production I simply do git push live and it will push it to the server. Once received the server will run a custom post-receive hook that builds the app's frontend and compiles the backend code using Babel. When everything is done it will restart the app using PM2. I use the same setup for git push staging to push to a staging environment.

Backups

I use Digital Ocean's snapshots and backup service to backup the whole droplet. This might not scale later on but for now it works great and I can be sure to have the full environment backed up and ready to be restored in case of emergency.

Summary

This is no silver bullet and it's not very fancy either. In fact, it's very basic and unhip. To make choices you have to know your goals and what you want to optimize for. So far this has worked for me while being on the front page of HN and Product Hunt. Working with modern Javascript can sometimes be a challenge since things change in a rapid pace. But as long as you enjoy the ride, I don't see a big issue with it. Lastly, I'm going to write in-depth articles on some of these topics so if you have any requests, let me know 😊.

Talk about code, startups or something else? Say Hi 👋 on Twitter @drikerf.