diff --git a/docs/middleware/csrf.md b/docs/middleware/csrf.md
index e69de29..c1b36fc 100644
--- a/docs/middleware/csrf.md
+++ b/docs/middleware/csrf.md
@@ -0,0 +1,102 @@
+# csrf middleware
+
+```python
+from spiderweb import SpiderwebRouter
+
+app = SpiderwebRouter(
+ middleware=[
+ "spiderweb.middleware.sessions.SessionMiddleware",
+ "spiderweb.middleware.csrf.CSRFMiddleware",
+ ],
+)
+```
+
+> [!DANGER]
+> The CSRFMiddleware is incomplete at best and dangerous at worst. I am not a security expert, and my implementation is [very susceptible to the thing it is meant to prevent](https://en.wikipedia.org/wiki/Cross-site_request_forgery). While this is an big issue (and moderately hilarious), the middleware is still provided to you in its unfinished state. Be aware.
+
+Cross-site request forgery, put simply, is a method for attackers to make legitimate-looking requests in your name to a service or system that you've previously authenticated to. Ways that we can protect against this involve aggressively expiring session cookies, special IDs for forms that are keyed to a specific user, and more.
+
+Notice that in the example above, SessionMiddleware is also included in the middleware list. The CSRF middleware requires the SessionMiddleware to function, and SessionMiddleware must be placed above it in the middleware list.
+
+## CSRF and Forms
+
+When you create a form, submitting data to the form is the part where things can go wrong. The CSRF middleware grants you two extra pieces in the TemplateResponse response: `csrf_token` and `csrf_token_raw`. `csrf_token` is a preformatted HTML input with preset attributes, ready for use, that you can drop into your template, while `csrf_token_raw` is the token itself with no extra formatting in case you'd like to do something else with it.
+
+Here's an example app that renders a form with two input fields and a checkbox, accepts the form data, and sends back the information as JSON.
+
+```python
+# myapp.py
+from spiderweb import SpiderwebRouter
+from spiderweb.response import JsonResponse, TemplateResponse
+
+app = SpiderwebRouter(
+ templates_dirs=["templates"],
+ middleware=[
+ "spiderweb.middleware.sessions.SessionMiddleware",
+ "spiderweb.middleware.csrf.CSRFMiddleware",
+ ],
+)
+
+@app.route("/", allowed_methods=["GET", "POST"])
+def form(request):
+ if request.method == "POST":
+ return JsonResponse(data=request.POST)
+ else:
+ return TemplateResponse(request, "form.html")
+```
+
+```html
+
+
+
+
+
+
+ Bootstrap demo
+
+
+
+
+
+
+
+
+
+```
+
+With this complete app, it will serve and accept the form. Note towards the bottom of `form.html` the line `{{ csrf_token }}` — this is the inserted key. All you need to do is make sure that line is included inside your form and it will be accepted and parsed.
+
+## Marking views as CSRF-Exempt
+
+```python
+from spiderweb.decorators import csrf_exempt
+```
+
+If you want to accept POST data at an endpoint with CSRF verification enabled, you will need to mark the endpoint as CSRF-exempt. Spiderweb provides a decorator for this use case:
+
+```python
+@csrf_exempt
+@app.route("/form", allowed_methods=["GET", "POST"])
+def form(request):
+ ...
+```
+
+Just drop it above the route information for your function and Spiderweb will not check for a CSRF token when form data is submitted.
diff --git a/docs/middleware/overview.md b/docs/middleware/overview.md
index 475c9a3..42385c7 100644
--- a/docs/middleware/overview.md
+++ b/docs/middleware/overview.md
@@ -16,4 +16,13 @@ app = SpiderwebRouter(
)
```
-Middleware affects both the incoming request AND the outgoing response, so the order that it's defined here is important. When a request comes in, it will be processed through the middleware in the order that it's defined, but after the response is created, it will pass back through the middleware going the opposite direction.
\ No newline at end of file
+Middleware affects both the incoming request AND the outgoing response, so the order that it's defined here is important. When a request comes in, it will be processed through the middleware in the order that it's defined, but after the response is created, it will pass back through the middleware going the opposite direction. Each piece of middleware has two functions: `process_request` and `process_response`.
+
+When a request comes in, after building the request object, Spiderweb will start running through the middleware from the top down. In the example above, that means that it will call `process_request()` on the SessionMiddleware, then CSRFMiddleware, and finally PydanticMiddleware. At this point, control is passed to the view so that you can run your application-specific logic. As part of the view, you'll return a response, and Spiderweb will take over again.
+
+Now that Spiderweb has the response object, it will start from the bottom of the stack (PydanticMiddleware in the example) and call `process_response()`, working its way up back to the top and ending with the `process_response()` of SessionMiddleware.
+
+> [!NOTE]
+> Middleware must be declared when instantiating the server; they can't be added on afterward. Once the server has been started, every piece of middleware will run for every request and every response.
+
+There are a few pieces of middleware built into Spiderweb that you can immediately put to use; they're described on the following pages.
diff --git a/docs/responses.md b/docs/responses.md
index a4f9cb1..a2d4dce 100644
--- a/docs/responses.md
+++ b/docs/responses.md
@@ -101,6 +101,9 @@ def index(request):
)
```
+> [!TIP]
+> You can [read more about crafting templates for Jinja here!](https://jinja.palletsprojects.com/en/3.0.x/templates/)
+
## RedirectResponse
```python
diff --git a/templates/form.html b/templates/form.html
index 3cbe891..84da6b4 100644
--- a/templates/form.html
+++ b/templates/form.html
@@ -14,7 +14,7 @@
-
+ {{ csrf_token }}