<p><b>Note</b>: as I'm constantly learning new things, this blog post is sort of outdated now. The information is still correct, but it feels inefficient and leads into a dead-end. I dislike this, as I wanted to write a guide that provides an expandable base. Therefore, I'll likely either update or replace this article and trim it down to focus on the important bits.</p>
Do you want to develop a web application in SvelteKit? Do you want this application to access a PostgreSQL database on a remote server? Do you struggle with CORS (cross-origin resource sharing) and CSRF (cross-site request forgery) and keep receiving error 403? Well, I did, and I couldn't find ***ANYTHING*** on the internet going beyond a test running locally. This is why I'm writing this guide.
Let me preface this by saying: what I did isn't perfect. It's also likely incomplete; I would give bad advice in a complete guide for sure. What this is, is a guide covering the key aspects for setting up a relatively minimal setup to get your application online successfully, upon which you can expand by adding more features. The important distinction from other guides I've found online is that this guide doesn't deploy the backend locally, but instead sets it up online, ready to use.
## The Guide
### What You Need
- a device, such as a laptop or a desktop, with the following installed:
- an IDE
-`npm`
-`ssh` keys for the server, if you're accessing it remotely
- a remote server instance set up with some software:
- Nginx
- Docker Compose
- an idea for what kind of app you wanna make
### Getting Started
#### SvelteKit
For creating a SvelteKit app, I recommend the guide [Getting Started](https://svelte.dev/docs/svelte/getting-started) by the Svelte team. You won't need more than the first four commands to create the app and get it up and running. During the setup, I recommend you pick the `adapter-node`.
#### Backend
For the backend, which will run on your remote server, it's very easy to use Docker Compose to get started. Once you're `ssh`'d into the server (or physically sitting at the terminal), create a folder where the data will live in, then create a file named `compose.yml` (for instance, by typing `nano compose.yml` into the terminal) and paste this into the file, then use `docker compose up -d` (or if that doesn't work, try `docker-compose up -d` with a hyphen) to start the container:
This `compose.yml` covers both the PostgreSQL database itself as well as a program called [PostgREST](https://docs.postgrest.org/), which connects to a PostgreSQL database and acts as a [REST](https://developer.mozilla.org/en-US/docs/Glossary/REST) api. This is advantageous, as it means you won't need modules such as [Postgres.js](https://github.com/porsager/postgres) to send raw SQL queries from SvelteKit to the database. With PostgREST, you can use a regular URL to access and modify database data using HTTP verbs such as GET (for getting data) and POST (for sending new data).
Some environment variables need to be set: `PGRST_SERVER_CORS_ALLOWED_ORIGINS` decides from which domains requests are allowed. Setting it to `"*"` allows all domains. Change this to the domain your application is running on. Set `POSTGRES_USER` and `POSTGRES_PASSWORD` to the data you'll use to log in as your admin account. Also, `PGRST_DB_ANON_ROLE` is the user that will be used when making requests to the database without credentials. This user's permissions will decide whether GET, POST, etc. requests will go through. You don't need to change this, but if you do, you'll also have to change `web_anon` later in this guide.
In `PGRST_DB_URI`, the link uses the username `authenticator` and a password. Replace the word 'password' with the actual password you'll use for the `authenticator` role. You will set up this role [later](#configure-the-database). This is *not* the same password you'll set for `POSTGRES_PASSWORD`!
The ports may differ on your machine. The `database` service runs on port 5432. You only need to change this if this port is already used by something on your server. Keep in mind to change the **left** number only! The left number is the port that faces outwards, the right port is the one used internally by the container. Since PostgreSQL is running on port 5432 by default, and since there is nothing else running inside the container that's also on port 5432, we don't need to change the right side at all. If you change the left port, be sure to change the port used in the `api` service's `PGRST_DB_URI` as well, as this URI is used by PostgREST to connect to the database.
I did change the port for the `api` service, however. PostgREST runs on port 3000 by default. This is problematic, as Node (which we will use for the SvelteKit application later) also runs on port 3000 by default. I chose to change PostgREST's port to 3100 here to avoid conflict, but you can pick any non-privileged port (any between 1024 and 65535 is good).
Keep in mind that you will not need to open up any of these ports on your firewall. Why? This is why:
I'm using Nginx as a reverse proxy here. What does this mean? It means that we can send a human-readable URL to Nginx, such as this: `https://files.natconf.dev`, and Nginx will *internally* route us to the place the service is actually running on: `http://localhost:3923`. This has several advantages: as the end user, we won't need to remember the IP address or the port of the services we connect to. As the server administrator, we will only need to expose a minimal number of ports. In this example, port 3923 is actually closed off from the public, meaning that bypassing Nginx and connecting to the service directly, e.g. by typing `https://natconf.dev:3923`, is impossible. The only open port is 443, which is a standard port for websites served over encrypted HTTPS. Your browser will always try to connect to a website starting with `https://` on port 443, unless you specify another port. Port 80 is actually also open on my server, as it accepts requests through the unsafe `http://`, but it only redirects to `https://` on 443.
I configured Nginx to run both the SvelteKit app as well as the PostgREST backend from the same subdomain. You don't *need* to do this, and I *think* it should even work if you put, for example, the database on a different subdomain, but I can't guarantee it.
<!-- { == {. if this is not encoded, sveltekit will try to interpret it as code and throw an error -->
- it serves our SvelteKit app, which by default runs on port 3000, from the root directory of the subdomain (e.g. `https://app.natconf.dev`), and
- it serves the table `customers` from our PostgreSQL database, which is accessible through PostgREST on port 3100, on the subpage `/customers` (`https://app.natconf.dev/customers`).
- If your table has a different name, replace `customers` with that table's name.
- If you want multiple tables to be accessible, copy the entire `location /[tablename]` block and paste it in as often as you need, replacing `[tablename]` with the names of the tables.
Once you have the database running, we need to create a table to store data in. You can log into the database on your server by running this command to enter the PostgreSQL console: `docker exec -it [service-name] psql -d postgres -U [username]`. `[container-name]` is the name of the PostgreSQL service, which you will see popping up in the terminal after running `docker compose up -d`. It will likely look something like `foldername_database`. `[username]` is the name of the privileged user defined in `compose.yml` under the environment variable `POSTGRES_USER`. This is basically your admin account.
You're best off following [the official guide for PostgREST](https://docs.postgrest.org/en/v14/tutorials/tut0.html#step-3-create-database-for-api). Important steps: create the schema, name it `api` (that's the publicly-accessible schema because we set `PGRST_DB_SCHEMAS: 'api'` in the `compose.yml`), create a table (name has to start with `api.` but can be anything after that, e.g. `api.customers`), insert test data, create the roles `web_anon` and `authenticator` and then quit by typing `\q`. That's all. Welcome back!
You can now access the database data on `https://app.natconf.dev` (if you replace my domain with yours). Keep in mind that the PostgREST guide only grants read access (GET) to the database; in order to POST data to the database, you need to grant `INSERT` privileges to `web_anon`: `grant insert on api.customers to web_anon;`.
If you want a kind voice to guide you through this process, I found [Ian Wootten's video on setting up PostgREST](https://youtu.be/RxuofiZNhtU) to be very nice to follow along.
### POST Data From SvelteKit
The difficult part is done. Sending data from SvelteKit to be inserted into the database is pretty easy, fortunately. You can use `fetch` to do this:
In this example, I set up two environment variables: `API_HOST` and `API_DB`. Those are secrets that won't be published to your Git provider when you push your changes (if your .gitignore is set up correctly!). `API_HOST` is the fully-qualified domain, e.g. `https://app.natconf.dev`, and `API_DB` is the name of the table, e.g. `customers`. Doing this is not necessary, but it's good practice to keep secrets, especially once you get to the stage of needing passwords or authentication keys. If you want to use environment variables, create a file named `.env` in the root directory of your application, add the variables as such:
Keep in mind that the `fetch` call is async, meaning that if you wrap this in a function, you will have to declare the function as such, e.g. `async function insertCustomer(...)` and call it using await: `await insertUser(...)`.
Also, the `customer` variable I declared is basically one database entry with the exact same properties as you set it up in PostgreSQL, except for the primary key (likely named `id`). You shouldn't send this in a POST request, as `id` will be auto-generated by the database to be a unique value. That is, unless you *want* to generate these values yourself, in which case, go ahead.
This should now work fine to POST data to the database...
### Fix Error 403 On SvelteKit
...except in production. Once you deploy the application on your server, you may face error *'403: Cross-site POST form submissions are forbidden'*. This is an issue caused by a security measure implemented in the browser that forbids accepting data unless the server sends the correct headers. You will only see this issue in a production build as SvelteKit disables these security measures when you're running a development build. The way I fixed this in production is by adding the domain of my backend to the `trustedOrigins` inside the `kit` block of my `svelte.config.js`:
**This is the crucial bit.** Only by adding the URL of my backend to the `trustedOrigins` was I able to fix the 403 errors. Is this the correct way of doing it? Frankly, I don't know. But I do know that you should definitely not put `"*"` as one of the trusted origins, as this would mean your app accepts data from anywhere, and that is *definitely* unsafe.
If you just came here to get a solution for the 403 error – that's it! We're done :) But read on for an extra piece of advice as well as my struggle to get this working.
### Your Data Will Be Public
**Important:** the contents of the database will be publicly visible, as we've granted GET (and possibly INSERT) permissions to `web_anon`. You will need to do a little more work to create a safe API that only exposes required data. Use either [PostgREST's guide](https://docs.postgrest.org/en/v14/tutorials/tut1.html) or [Ian Wootten's video](https://youtu.be/RxuofiZNhtU) for guidance on how to restrict and grant access safely.
## The Rant
If you only came for the guide, you've finished it. What follows is me ranting about how difficult it was to get this information.
### Why Did I Write This?
I was building a small SvelteKit application for a project I'm doing together with a friend. I figured it'd be easy, as it's just like building my website, except I'll also make some requests to a backend on my server. I was right... kind of. I have spent *two days* trying to figure out how to do this successfully.
The requests were working fine when I was running the development build of the SvelteKit app on my PC. Once I deployed it though, I was facing *'403: Cross-site POST form submissions are forbidden'* errors for every single request. I couldn't figure out why. I scoured the internet for answers, trying anything I could. I configured `Access-Control-Allow-*` headers in Nginx. I set the `ORIGIN` parameter in the app's `.env`. I ran the app and the database on the same subdomain. Nothing worked. When I tried looking for tutorials, there was *nothing* explaining how I could set up what I had in mind.
How could this be? I'm doing web development 101 here! This isn't some advanced toolchain I'm using, it's the bare minimum for a working application with a backend! Somehow, no resource I could find covered this. Any website I read and any video I watched only ever ran those services locally. Docker containers were spun up on the development client, the client apps were run in development mode (which disables CORS/CSRF security features), and there was never a *live* example given.
### Stupid Gaslighting AI Slop
These days, when looking up tech problems in particular, most of the online resources are AI-generated slop. It was genuinely extremely difficult to avoid them, as they were not just prevalent, but almost entirely dominating the search results. There was this one page that I particularly despised. The worst part? It was actually several pages.
If you stumble upon a page such as `w3tutorials.net` or `tutorialpedia.org`, *run*. Do not take in their information. Avoid at all costs. In fact, these pages both use the very same CSS styling with their dark blue background and fade-in title animation upon page load, and you will find even more domains using this exact same template.
But why run? Is it really that bad? Maybe the information is useful?
I'm going to prove why you should avoid generative AI at all costs. Let's go through the article I read: "How to Fix 'Cross-site POST Form Submissions Are Forbidden' Error in SvelteKit: Dev Server Works, Build Fails". Extremely long title, but it in fact describes my exact problem. Four 'fixes' are given:
This 'fix' explains that the issue may be caused by the lack of a CSRF token included in the form that would verify the validity of the submission. The solution is to import the token into the app using `import { csrfToken } from '$app/stores';` and then including the token in a hidden `<input>` named `_csrf`.
*What in the hell* did this AI smoke to hallucinate this? There is no such thing as an importable CSRF token, and it wouldn't make sense for the solution to just be 'send the token along with the request'. Who's validating the token? How would they know it's valid? You would need server-side validation of the token, which is a real thing, but you'd need to code that yourself or use a library for it. You can't just send a random string and say "this request is real, here's some random text to prove it".
3. Configure `ORIGIN` and Environment Variables
Ignoring the fact that their markdown styling doesn't work correctly and shows the \` marks on text that is supposed to be monospaced, this 'fix' alleges that setting the `ORIGIN` environment variable needs to be set to the domain the application is running on so that SvelteKit is aware of this domain.
This *may* work? SvelteKit documentation does [make mention of an `ORIGIN` variable](https://svelte.dev/docs/kit/adapter-node#Environment-variables-ORIGIN-PROTOCOL_HEADER-HOST_HEADER-and-PORT_HEADER) used in the Node adapter. However, as far as I could tell, this had no effect on my app.
4. Check Server Hooks (Handle Function)
This 'fix' accuses you, the developer, of explicitly deleting the required headers for CSRF validation using code such as: `delete event.request.headers.origin;`. *Excuse me?* You little piece of shit. How dare you accuse me of something like this? No, of course I haven't explicitly deleted the headers from my request!
Actually, now that I think about it, this is a very odd thing for an AI to produce. Usually, they are overly positive towards the user. I guess this one was still positively phrased and only indirectly accusing the reader of the mistake. Still, the implication is ridiculous.
### No Surprise Stack Overflow is Dying
Alas, I couldn't find anything helping my cause. In my most desperate moment, I came up with an idea I never thought I'd pursue: *asking a question on Stack Overflow*. **THE** Stack Overflow! The place where arrogant people get high on the most minuscule amounts of power – or so I've heard. It can't be that bad.
I wasn't entirely sure whether my problem lied in my SvelteKit, Nginx, or PostgreSQL setup, so I documented them briefly but with the necessary detail in my question. I described my plan and my previous attempts at fixing the issue at hand, including links to the resources I had used. I formatted the entire thing and structured it so that it would be relatively simple to digest. It also had a concluding question to summarise my problem at the end. I read their entire stupid article on 'writing a good question'. What a ridiculous article.
I submitted it to the Staging Ground, which is apparently a Stack Overflow-exclusive thing where new users are forced (?) to submit their questions for 'experienced' users to judge them (which is such a fucking weird concept) and give advice. Fair enough, I thought, I have a fairly open mind and I'd gladly receive constructive feedback so that others could answer my question better.
What instead happened was that within less than 10 minutes of posting my question, two users deemed my question 'off-topic' with the reason 'this question is not about programming or software development' and closed it before it went public. wow. One of them at least had the decency of giving me the advice to post in either Server Fault or Database Administrators. Seeing that this *definitely* wasn't a database problem, I deemed the latter suggestion misguided at best, but I did try Server Fault instead. I copy-pasted my question, submitted it directly (they had no such thing as a Staging Ground), and proceeded to receive no answers.
The best part: once I figured out the solution to my problem on my own, it became abundantly clear that my problem was caused by SvelteKit. This means:
- the question was actually off-topic on Server Fault
- the question was indeed correct to be posted on Stack Overflow
- whoever judged my question to be 'off-topic' likely only wanted to exercise their virtual 'power' and didn't even bother reading and understanding my question while expecting me to read paragraphs on 'how to ask a good question'
You know what? Stack Overflow can burn in hell. No wonder its user base is continually shrinking. What a horrible, toxic place. I'm saying this mostly because of the things I've *heard* about Stack Overflow, but my short first-hand experience clearly showed that those things are true.