Skip to content

Commit be3b2f6

Browse files
committed
doc: add README
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
1 parent deceddd commit be3b2f6

File tree

7 files changed

+180
-78
lines changed

7 files changed

+180
-78
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
TIGRIS_STORAGE_ACCESS_KEY_ID=tid_access_key_id
2+
TIGRIS_STORAGE_SECRET_ACCESS_KEY=tsec_secret_access_key
3+
TIGRIS_STORAGE_ENDPOINT=https://t3.storage.dev
4+
TIGRIS_STORAGE_BUCKET=your-bucket-here

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# pastebin
2+
3+
This is a simple [Pastebin](https://pastebin.com) clone where every paste is backed by [Tigris](https://tigrisdata.com/) and [Keyv](https://keyv.org/) using [@tigrisdata/keyv-tigris](https://www.npmjs.com/package/@tigrisdata/keyv-tigris).
4+
5+
To get started:
6+
7+
1. Create a bucket at [storage.new](https://storage.new).
8+
2. Create an [access key](https://www.tigrisdata.com/docs/iam/manage-access-key/) for that bucket with Editor permissions.
9+
3. Copy `.env.example` to `.env`.
10+
4. Start the service in Docker:
11+
12+
```text
13+
docker run --name pastebin --env-file .env -p 3333:3333 ghcr.io/tigrisdata-community/pastebin
14+
```
15+
16+
Then open your browser to [localhost:3333](http://localhost:3333) and post away! All your posts will be seamlessly stored across the globe in Tigris.
17+
18+
## How it works
19+
20+
This app is an [Express.js](https://expressjs.com/) app written with [HTMX](https://htmx.org/) to make the website interactive. If you are using an environment where JavaScript is disabled, this app will not function.
21+
22+
Creating a paste inserts a JSON object like this into Tigris:
23+
24+
```json
25+
{
26+
"value": {
27+
"id": "019acb05-df16-758f-a4fe-c5026f54e12e",
28+
"title": "arst",
29+
"body": "arst",
30+
"createdAt": "2025-11-28T15:12:31.000Z"
31+
}
32+
}
33+
```
34+
35+
When you visit a paste URL (eg: `/paste/019acb05-df16-758f-a4fe-c5026f54e12e`), Keyv loads the paste data out of Tigris and renders it in the browser.
36+
37+
The cool part about this flow is that the server returns only the bit of HTML on the page that needs to change:
38+
39+
```html
40+
<h2 class="text-3xl font-bold text-gray-800 mb-4">
41+
This is a test of HTMX and express.js
42+
</h2>
43+
<pre class="bg-gray-100 rounded p-4 text-gray-800 mb-6 whitespace-pre-wrap">
44+
Honestly this is really cool, I love how the server just deals with HTML instead of having to convert between JSON and HTML!</pre
45+
>
46+
<div class="flex flex-col gap-2 text-sm text-gray-600">
47+
<div>
48+
<span class="font-semibold">ID:</span>
49+
<span class="font-mono">019acb3f-0e5e-7e37-b65e-3cb2c10d8faa</span>
50+
</div>
51+
<div>
52+
<span class="font-semibold">Created At:</span>
53+
<span class="font-mono">2025-11-28T16:14:58.655Z</span>
54+
</div>
55+
</div>
56+
```
57+
58+
Please [dig through the source code](./src/main.ts) and try this pattern out for yourself! Tigris' globally distributed and consistent object storage makes for a great backend for simple apps like this. Try enabling lifecycle deletion and see how that changes what you do! The sky's the limit!

src/main.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { engine } from 'express-handlebars';
44
import Handlebars from 'handlebars';
55
import Keyv from 'keyv';
66
import { KeyvTigris } from '@tigrisdata/keyv-tigris';
7-
import KeyvSqlite from '@keyv/sqlite';
87
import { uuidv7 } from "uuidv7";
98
import { body, matchedData, validationResult } from "express-validator";
109

@@ -39,15 +38,11 @@ Handlebars.registerHelper('formatRFC3339', function (date) {
3938
}
4039
});
4140

42-
const store = new KeyvTigris({
43-
bucket: process.env['TIGRIS_BUCKET'],
44-
accessKeyId: process.env['TIGRIS_STORAGE_ACCESS_KEY_ID'],
45-
secretAccessKey: process.env['TIGRIS_STORAGE_SECRET_ACCESS_KEY'],
46-
endpoint: process.env['TIGRIS_STORAGE_ENDPOINT'],
47-
});
48-
//const store = new KeyvSqlite('sqlite://./var/database.sqlite');
41+
const store = new KeyvTigris();
4942
const pastes = new Keyv<Paste>({ store, namespace: "paste" });
5043

44+
pastes.on('error', err => console.error("Store error:", err));
45+
5146
// Routes
5247
app.get('/', (req: express.Request, res: express.Response) => {
5348
if (req.headers['hx-request'] === 'true') {

src/views/error.handlebars

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,55 @@
11
<div class="bg-white rounded-lg shadow-md p-6">
2-
<div class="text-center">
3-
<svg class="mx-auto h-12 w-12 text-red-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
4-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
5-
</svg>
6-
7-
<h2 class="text-3xl font-bold text-gray-800 mb-4">{{title}}</h2>
8-
9-
<p class="text-gray-600 mb-4">{{message}}</p>
10-
11-
{{#if details}}
12-
<ul class="text-left bg-red-50 border border-red-200 rounded-md p-4 mb-4 max-w-md mx-auto">
13-
{{#each details}}
14-
<li class="text-sm text-red-700 mb-1 last:mb-0">• {{this}}</li>
15-
{{/each}}
16-
</ul>
17-
{{/if}}
18-
19-
{{#if canRetry}}
20-
<p class="text-gray-500 text-sm mb-6">If you believe you have reached this page in error, please retry your last action.</p>
21-
<div class="flex items-center justify-center gap-x-4">
22-
<a href="/" class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
23-
Go Home
24-
</a>
25-
</div>
26-
{{else}}
27-
<p class="text-gray-500 text-sm mb-6">Please contact the server administrator for assistance.</p>
28-
<div class="flex items-center justify-center">
29-
<a href="/" class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
30-
Go Home
31-
</a>
32-
</div>
33-
{{/if}}
34-
</div>
35-
</div>
2+
<div class="text-center">
3+
<svg
4+
class="mx-auto h-12 w-12 text-red-500 mb-4"
5+
fill="none"
6+
viewBox="0 0 24 24"
7+
stroke="currentColor"
8+
>
9+
<path
10+
stroke-linecap="round"
11+
stroke-linejoin="round"
12+
stroke-width="2"
13+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
14+
/>
15+
</svg>
16+
17+
<h2 class="text-3xl font-bold text-gray-800 mb-4">{{title}}</h2>
18+
19+
<p class="text-gray-600 mb-4">{{message}}</p>
20+
21+
{{#if details}}
22+
<ul
23+
class="text-left bg-red-50 border border-red-200 rounded-md p-4 mb-4 max-w-md mx-auto"
24+
>
25+
{{#each details}}
26+
<li class="text-sm text-red-700 mb-1 last:mb-0">• {{this}}</li>
27+
{{/each}}
28+
</ul>
29+
{{/if}}
30+
31+
{{#if canRetry}}
32+
<p class="text-gray-500 text-sm mb-6">If you believe you have reached this
33+
page in error, please retry your last action.</p>
34+
<div class="flex items-center justify-center gap-x-4">
35+
<a
36+
href="/"
37+
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
38+
>
39+
Go Home
40+
</a>
41+
</div>
42+
{{else}}
43+
<p class="text-gray-500 text-sm mb-6">Please contact the server
44+
administrator for assistance.</p>
45+
<div class="flex items-center justify-center">
46+
<a
47+
href="/"
48+
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
49+
>
50+
Go Home
51+
</a>
52+
</div>
53+
{{/if}}
54+
</div>
55+
</div>

src/views/index.handlebars

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
<div class="bg-white rounded-lg shadow-md p-6">
2-
<h2 class="text-3xl font-bold text-gray-800 mb-4">Share your text easily!</h2>
3-
<form id="new-paste" hx-post="/submit" hx-target="main">
4-
<div>
5-
<label for="title" class="block text-sm/6 font-medium text-gray-900">Title</label>
6-
<div class="mt-2">
7-
<input id="title" type="text" name="title" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" />
8-
</div>
9-
</div>
10-
<div>
11-
<label for="paste" class="block text-sm/6 font-medium text-gray-900">Paste</label>
12-
<div class="mt-2">
13-
<textarea id="paste" name="paste" rows="4" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"></textarea>
14-
</div>
15-
</div>
16-
<div class="mt-6 flex items-center justify-end gap-x-6">
17-
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Submit</button>
18-
</div>
19-
</form>
20-
</div>
21-
2+
<h2 class="text-3xl font-bold text-gray-800 mb-4">Share your text easily!</h2>
3+
<form id="new-paste" hx-post="/submit" hx-target="main">
4+
<div>
5+
<label
6+
for="title"
7+
class="block text-sm/6 font-medium text-gray-900"
8+
>Title</label>
9+
<div class="mt-2">
10+
<input
11+
id="title"
12+
type="text"
13+
name="title"
14+
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"
15+
/>
16+
</div>
17+
</div>
18+
<div>
19+
<label
20+
for="paste"
21+
class="block text-sm/6 font-medium text-gray-900"
22+
>Paste</label>
23+
<div class="mt-2">
24+
<textarea
25+
id="paste"
26+
name="paste"
27+
rows="4"
28+
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"
29+
></textarea>
30+
</div>
31+
</div>
32+
<div class="mt-6 flex items-center justify-end gap-x-6">
33+
<button
34+
type="submit"
35+
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
36+
>Submit</button>
37+
</div>
38+
</form>
39+
</div>

src/views/layouts/main.handlebars

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1-
<!DOCTYPE html>
21
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
65
<title>{{title}}</title>
76
<script src="/js/htmx.min.js"></script>
8-
<link href="/output.css" rel="stylesheet">
9-
</head>
10-
<body class="min-h-screen bg-gray-100">
7+
<link href="/output.css" rel="stylesheet" />
8+
</head>
9+
<body class="min-h-screen bg-gray-100">
1110
<nav class="bg-blue-600 text-white p-4">
12-
<div class="container mx-auto">
13-
<h1 class="text-2xl font-bold"><a href="/">Pastebin</a></h1>
14-
</div>
11+
<div class="container mx-auto">
12+
<h1 class="text-2xl font-bold"><a
13+
href="/"
14+
hx-target="main"
15+
hx-get="/"
16+
>Pastebin</a></h1>
17+
</div>
1518
</nav>
1619

1720
<main class="container mx-auto p-8 mx-auto max-w-5xl" id="main">
18-
{{{body}}}
21+
{{{body}}}
1922
</main>
20-
</body>
23+
</body>
2124
</html>

src/views/paste.handlebars

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<h2 class="text-3xl font-bold text-gray-800 mb-4">{{paste.title}}</h2>
2-
<pre class="bg-gray-100 rounded p-4 text-gray-800 mb-6 whitespace-pre-wrap">{{paste.body}}</pre>
2+
<pre
3+
class="bg-gray-100 rounded p-4 text-gray-800 mb-6 whitespace-pre-wrap"
4+
>{{paste.body}}</pre>
35
<div class="flex flex-col gap-2 text-sm text-gray-600">
4-
<div><span class="font-semibold">ID:</span> <span class="font-mono">{{paste.id}}</span></div>
5-
<div><span class="font-semibold">Created At:</span> <span class="font-mono">{{formatRFC3339 paste.createdAt}}</span></div>
6-
</div>
6+
<div><span class="font-semibold">ID:</span>
7+
<span class="font-mono">{{paste.id}}</span></div>
8+
<div><span class="font-semibold">Created At:</span>
9+
<span class="font-mono">{{formatRFC3339 paste.createdAt}}</span></div>
10+
</div>

0 commit comments

Comments
 (0)