Learn about how you can implement authenticated routes in Kitten by simply appending a lock emoji (🔒) to the file names of routes as you build a simple guestbook example that uses the POST/redirect/GET pattern and JSDB to store guestbook messages in a database.

Topics covered

Secrets and lies (OK, just secrets, actually.)

Kitten is more than just a regular web server, it is a Small Web server. And the whole idea behind the Small Web is that it is a peer-to-peer web of individually-owned and controlled web places.

In such a setup, there will be certain features of your web place and you and you alone should have access to. We call these private routes. And, you should also be able to communicate with everyone else’s Small Web place in private. In other words, using end-to-end encryption.

Kitten’s automatic support for cryptographic identities and authentication makes implementing private features easy.

Your cryptographic identity

Kitten automatically generates a cryptographically-secure secret for each project.

💡 More precisely, the key is different for each deployment of the project, as it is the key for the person who owns and controls that instance of the project.

For the cryptographers among you, this is a base256-encoded ed25519 private key that happens to use emoji for its set of printable characters. For everyone else, it’s a lovely string of 32 emoji that looks something like this:


This secret is shown to you only the very first time you run Kitten on a given project (folder).

💡 When your app is deployed by everyday people who use technology as an everyday thing using Domain, they will see the secret URL in the browser as part of their process of obtaining hosting for their Small Web place. The geeky link in terminal is just for us developers to use while building Small Web apps.

If you’re wondering how in the world you are going to type that in, don’t worry: you’re not supposed to be able to type it in. This is by design.

Instead, please add this secret to your password manager of choice.

If you implement authenticated routes in your application, you can use your password manager to enter your secret for you.

For technical details, please see the Cryptographical Properties section of the Kitten Referenence.

Authenticated Routes

To signal to Kitten that a route is only available when the person is authenticated, you add 🔒 to the end of the route name (that’s a lock emoji).

Kitten itself has just a route that’s available to all apps and sites created with Kitten at:


💡 The /💕/ path has routes that are common to all Small Web apps. Emoji problem? I don’t have an emoji problem… you have an emoji problem! 👀

If you hit the /💕/settings route on any Kitten app or site, you’ll be automatically redirected to the /💕/sign-in route if you’re not authenticated and you only see the settings section if you are.

Adding the suffix to a directory, as shown here, ensures that it applies to all routes in that directory.

You can also add it to specific routes (files).

💡 We add the lock emoji as a suffix instead of a prefix so that we can easily make use of autocompletion when typing in the path.

HTTP Routes

We’ve seen examples of simple Kitten apps that use pages and WebSockets, but what if you want to POST data from a web form, implement Ajax with fragments of HTML using htmx, or create an Application Programming Interface (API) that returns JSON?

Enter HTTP Routes.

(OK, technically speaking, everything is an HTTP route but that’s the terminology we use in Kitten to separate Pages from, well, every other HTTP route except WebSocket routes.)

Similar to how you create pages in .page.js files and WebSocket routes in .socket.js files, HTTP routes are declared using a naming convention based on their filename extension which can be any valid HTTP1/1.1 method in lowercase (e.g., .get.js, .post.js, .patch.js, .head.js, etc.)

HTTP Routes do not carry out any processing on whatever value you return for them.

So you can return a fragment of HTML (e.g., a component) if you’re implementing Ajax, or use JSON.stringfy() to return a JSON repsonse, etc.


  ├ about
  │   ╰
  ├ todos
  │   ╰ index.get.js
  ╰ chat
     ╰ index.socket.js

Optionally, to organise larger projects, you can encapsulate your site within a src folder. If a src folder does exist, Kitten will only serve routes from that folder and not from the project root.


  ├ src
  │  ├
  │  ├
  │  ├ index.socket.js
  │  ╰ about
  │      ╰
  ├ test
  │   ╰ index.js


One very common HTTP Route is POST, usually used when you want to send data back from a page and persist it.

The Guestbook example (examples/guestbook), demonstrates this pattern in the simplest possible way.

First, let’s create a page that will display the form for signing the guestbook and existing guestbook entries:

if (!kitten.db.entries) kitten.db.entries = []

export default () => kitten.html`


  <form method='POST' action='/sign'>
    <label for='message'>Message</label>
    <textarea id='message' name='message' required></textarea>
    <label for='name'>Name</label>
    <input type='text' id='name' name='name' required />
    <button type='submit'>Sign</button>


  ${kitten.db.entries.length === 0 ?
    kitten.html`<p>Hey, no one’s signed yet… be the first?</p>`

    ${ => kitten.html`
        <p class='message'>${entry.message}</p>
        <p class='nameAndDate'>${} (${new Date(})</p>

    body { font-family: sans-serif; margin-left: auto; margin-right: auto; max-width: 20em; }
    label { display: block; }
    textarea, input[type='text'] { width: 100%; }
    textarea { height: 10em; }
    button { width: 100%; height: 2em; margin-top: 1em; font-size: 1em; }
    ul { list-style-type: none; padding: 0; }
    li { border-top: 1px dashed #999; }
    .message, .nameAndDate { font-family: cursive; }
    .message { font-size: 1.5em; }
    .nameAndDate { font-size: 1.25em; font-style: italic; text-align: right; }

This is very straightforward. Notice that we have a form for signing the guestbook and it’s just plain HTML.

<form method='POST' action='/sign'>
  <label for='message'>Message</label>
  <textarea id='message' name='message' required></textarea>
  <label for='name'>Name</label>
  <input type='text' id='name' name='name' required />
  <button type='submit'>Sign</button>

Its method is set to POST and its action is /sign. That means that when the submit button is pressed, it will carry out an HTTP POST request to the /sign route on our server.

In that route, we will save the new guestbook entry and then redirect the person’s browser back to the index page. This pattern of handling a POST request and then redirecting to a GET route (our pages are all GET routes), is called the POST/redirect/GET pattern.

So let’s create our POST route:

if (!kitten.db.entries) kitten.db.entries = []

export default ({ request, response }) => {
  // Basic validation.
  if (!request.body || !request.body.message || ! {
    return response.forbidden()

    message: request.body.message,


And that’s it.

💡 Kitten has a number of request and response helpers defined to make your life easier. You just used two of them, above: response.forbidden(), which returns a HTTP 403: Forbidden error and response.get(), which is an alias for response.seeOther(), which returns and HTTP 303: See Other response.

In this case, since we’re going a Post/Redirect/Get (PRG), using the get() alias makes the intent of our code clearer.

You could also have manually handled the direction like this:

response.statusCode = 303
response.setHeader('Location', '/')

(Which is exactly what Kitten does internally when you use the .get() / .seeOther() methods.)

For the full list of helpers, please see the request and response helpers section of the Kitten Reference.

Run kitten command on your project folder and visit https://localhost to see your guestbook.

💡 Notice that we’re doing some very basic validation to make sure that body of the request (which is where the form’s data is found) is as we expect it.

In case you’re worried about script inject, type <script>alert("Hehe, I just hacked you!")</script> in your message box. Try it out and see what happens. Kitten’s template engine automatically escapes interpolated string content to avoid such attacks. If you wanted to allow HTML through, Kitten provides a global kitten.safelyAddHtml() function you can call that sanisitises the input before allowing it. While it comes with intelligent defaults, you can also customise exactly what you want to let through or not.

You can test out the basic server-side validation using a basic curl command. First, let’s send a bad request and see what we get. In this case, we’re not sending any data at all:

curl --include --data-urlencode '' https://localhost/sign/

And we see that our validation works:

HTTP/1.1 403 Forbidden
Access-Control-Allow-Origin: *
Set-Cookie: sessionId=LbhwT0YqkVyAzQR0S-2KFGPa; Max-Age=28800000; Path=/; HttpOnly; Secure; SameSite=Strict
Date: Fri, 11 Aug 2023 12:36:23 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

(The --include flag is what tells curl to print out the response header we received.)

Finally, let’s send a valid request and sign the guestbook from the command-line like proper nerds:

curl --include --data-urlencode 'message=From curl with love.' --data-urlencode 'name=Curl' https://localhost/sign/

This time, we get a much nicer response:

HTTP/1.1 303 See Other
Access-Control-Allow-Origin: *
Set-Cookie: sessionId=LMw_wcABaqRNWyhYmF7fvHFJ; Max-Age=28800000; Path=/; HttpOnly; Secure; SameSite=Strict
Location: /
Date: Fri, 11 Aug 2023 12:37:35 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

It’s telling us that we should see the / route. So, let’s. Go back to your browser and refresh the main page and you should see the guestbook entry from curl.

Now that we’ve seen how your cryptographic identity, authentication, and HTTP routes work in Kitten (and learned how to implement the POST/redirect/GET pattern), let’s take a look at how Kitten handles file uploads next.

Next tutorial: Multipart forms and file uploads