Skip to content

Commit 5075d92

Browse files
committed
refactor: rename storageKey to objectKey for Tigris object storage
also write decision doc and documentation
1 parent 128c161 commit 5075d92

File tree

13 files changed

+191
-45
lines changed

13 files changed

+191
-45
lines changed

app/routes/resources+/note-images.$imageId.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export async function loader({ params }: Route.LoaderArgs) {
77
invariantResponse(params.imageId, 'Image ID is required', { status: 400 })
88
const noteImage = await prisma.noteImage.findUnique({
99
where: { id: params.imageId },
10-
select: { storageKey: true },
10+
select: { objectKey: true },
1111
})
1212
invariantResponse(noteImage, 'Note image not found', { status: 404 })
1313

14-
const { url, headers } = getSignedGetRequestInfo(noteImage.storageKey)
14+
const { url, headers } = getSignedGetRequestInfo(noteImage.objectKey)
1515
const response = await fetch(url, { headers })
1616

1717
const cacheHeaders = new Headers(response.headers)

app/routes/resources+/user-images.$imageId.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export async function loader({ params }: Route.LoaderArgs) {
77
invariantResponse(params.imageId, 'Image ID is required', { status: 400 })
88
const userImage = await prisma.userImage.findUnique({
99
where: { id: params.imageId },
10-
select: { storageKey: true },
10+
select: { objectKey: true },
1111
})
1212
invariantResponse(userImage, 'User image not found', { status: 404 })
1313

14-
const { url, headers } = getSignedGetRequestInfo(userImage.storageKey)
14+
const { url, headers } = getSignedGetRequestInfo(userImage.objectKey)
1515
const response = await fetch(url, { headers })
1616

1717
const cacheHeaders = new Headers(response.headers)

app/routes/settings+/profile.photo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export async function action({ request }: Route.ActionArgs) {
7575
intent: data.intent,
7676
image: {
7777
contentType: data.photoFile.type,
78-
storageKey: await uploadProfileImage(userId, data.photoFile),
78+
objectKey: await uploadProfileImage(userId, data.photoFile),
7979
},
8080
}
8181
}),

app/routes/users+/$username_+/__note-editor.server.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
5757
id: i.id,
5858
altText: i.altText,
5959
contentType: i.file.type,
60-
storageKey: await uploadNoteImage(userId, noteId, i.file),
60+
objectKey: await uploadNoteImage(userId, noteId, i.file),
6161
}
6262
} else {
6363
return {
@@ -75,7 +75,7 @@ export async function action({ request }: ActionFunctionArgs) {
7575
return {
7676
altText: image.altText,
7777
contentType: image.file.type,
78-
storageKey: await uploadNoteImage(userId, noteId, image.file),
78+
objectKey: await uploadNoteImage(userId, noteId, image.file),
7979
}
8080
}),
8181
),
@@ -119,7 +119,7 @@ export async function action({ request }: ActionFunctionArgs) {
119119
data: {
120120
...updates,
121121
// If the image is new, we need to generate a new ID to bust the cache.
122-
id: updates.storageKey ? cuid() : updates.id,
122+
id: updates.objectKey ? cuid() : updates.id,
123123
},
124124
})),
125125
create: newImages,

app/utils/auth.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export async function signupWithConnection({
180180
image: {
181181
create: {
182182
contentType: imageFile.type,
183-
storageKey: await uploadProfileImage(user.id, imageFile),
183+
objectKey: await uploadProfileImage(user.id, imageFile),
184184
},
185185
},
186186
},

docs/decisions/018-images.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Images
22

3-
Date: 2023-06-23
3+
Date: 2023-06-23 Updated: 2024-03-19
44

5-
Status: accepted (for now)
5+
Status: superseded by
6+
[040-tigris-image-storage.md](./040-tigris-image-storage.md)
67

78
## Context
89

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Title: Switch to Tigris for Image Storage
2+
3+
Date: 2025-02-20
4+
5+
Status: accepted
6+
7+
## Context
8+
9+
The Epic Stack previously stored uploaded images directly in SQLite using binary
10+
data storage. While this approach is simple and works well for small
11+
applications, it has several limitations (as noted in the previous decision
12+
[018-images.md](docs/decisions/018-images.md)):
13+
14+
1. Binary data in SQLite increases database size and backup complexity
15+
2. Large binary data in SQLite can impact database performance
16+
3. SQLite backups become larger and more time-consuming when including binary
17+
data
18+
4. No built-in CDN capabilities for serving images efficiently
19+
20+
## Decision
21+
22+
We will switch from storing images in SQLite to storing them in Tigris, an
23+
S3-compatible object storage service. This change will:
24+
25+
1. Move binary image data out of SQLite into specialized object storage
26+
2. Maintain metadata about images in SQLite (references, ownership, etc.)
27+
3. Leverage Tigris's S3-compatible API for efficient image storage and retrieval
28+
4. Enable better scalability for applications with many image uploads
29+
30+
To keep things lightweight, we will not be using an S3 SDK to integrate with
31+
Tigris and instead we'll manage authenticated fetch requests ourselves.
32+
33+
## Consequences
34+
35+
### Positive
36+
37+
1. Reduced SQLite database size and improved backup efficiency
38+
2. Better separation of concerns (binary data vs relational data)
39+
3. Potentially better image serving performance through Tigris's infrastructure
40+
4. More scalable solution for applications with heavy image usage
41+
5. Easier to implement CDN capabilities in the future
42+
6. Simplified database maintenance and backup procedures
43+
7. Tigris storage is much cheaper than Fly volume storage
44+
45+
### Negative
46+
47+
1. Additional external service dependency (though Fly as built-in support and no
48+
additional account needs to be created)
49+
2. Need to manage Tigris configuration
50+
3. Slightly more complex deployment setup
51+
4. Additional complexity in image upload and retrieval logic
52+
53+
## Implementation Notes
54+
55+
The implementation involves:
56+
57+
1. Setting up Tigris configuration
58+
2. Modifying image upload handlers to store files in Tigris
59+
3. Updating image retrieval routes to serve from Tigris
60+
4. Maintaining backward compatibility during migration (database migration is
61+
required as well as manual migration of existing images)
62+
5. Providing migration utilities for existing applications
63+
64+
## References
65+
66+
- [Tigris Documentation](https://www.tigris.com/docs)
67+
- Previous image handling: [018-images.md](docs/decisions/018-images.md)

docs/image-storage.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Image Storage
2+
3+
The Epic Stack uses [Tigris](https://www.tigris.com), an S3-compatible object
4+
storage service, for storing and serving uploaded images. Tigris is integrated
5+
tightly with Fly.io, so you don't need to worry about setting up an account or
6+
configuring any credentials.
7+
8+
## Configuration
9+
10+
To use Tigris for image storage, you need to configure the following environment
11+
variables. These are automatically set for you on Fly.io when you create storage
12+
for your app which happens when you create a new Epic Stack project.
13+
14+
```sh
15+
AWS_ACCESS_KEY_ID="mock-access-key"
16+
AWS_SECRET_ACCESS_KEY="mock-secret-key"
17+
AWS_REGION="auto"
18+
AWS_ENDPOINT_URL_S3="https://fly.storage.tigris.dev"
19+
BUCKET_NAME="mock-bucket"
20+
```
21+
22+
These environment variables are set automatically in the `.env` file locally and
23+
a mock with MSW is set up so that everything works completely offline locally
24+
during development.
25+
26+
## How It Works
27+
28+
The Epic Stack maintains a hybrid approach to image storage:
29+
30+
1. Image metadata (relationships, ownership, etc.) is stored in SQLite
31+
2. The actual image binary data is stored in Tigris
32+
3. Image URLs point to the local server which proxies to Tigris
33+
34+
### Database Schema
35+
36+
The database schema maintains references to images while the actual binary data
37+
lives in Tigris:
38+
39+
```prisma
40+
model UserImage {
41+
id String @id @default(cuid())
42+
userId String
43+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
44+
objectKey String // Reference to the image in Tigris
45+
contentType String
46+
createdAt DateTime @default(now())
47+
updatedAt DateTime @updatedAt
48+
49+
@@index([userId])
50+
}
51+
52+
model NoteImage {
53+
id String @id @default(cuid())
54+
noteId String
55+
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
56+
objectKey String // Reference to the image in Tigris
57+
contentType String
58+
createdAt DateTime @default(now())
59+
updatedAt DateTime @updatedAt
60+
61+
@@index([noteId])
62+
}
63+
```
64+
65+
### Image Upload Flow
66+
67+
1. When an image is uploaded, it's first processed by the application
68+
(validation, etc.)
69+
2. The image is then streamed to Tigris
70+
3. The metadata is stored in SQLite with a reference to the Tigris object key
71+
4. The image can then be served by proxying to Tigris
72+
73+
## Customization
74+
75+
For more details on customization, see the source code in:
76+
77+
- `app/utils/storage.server.ts`
78+
- `app/routes/resources+/note-images.$imageId.tsx`
79+
- `app/routes/resources+/user-images.$imageId.tsx`

prisma/migrations/20250220221138_init/migration.sql renamed to prisma/migrations/20250221001609_init/migration.sql

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ CREATE TABLE "NoteImage" (
2424
"id" TEXT NOT NULL PRIMARY KEY,
2525
"altText" TEXT,
2626
"contentType" TEXT NOT NULL,
27-
"storageKey" TEXT NOT NULL,
27+
"objectKey" TEXT NOT NULL,
2828
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2929
"updatedAt" DATETIME NOT NULL,
3030
"noteId" TEXT NOT NULL,
@@ -36,7 +36,7 @@ CREATE TABLE "UserImage" (
3636
"id" TEXT NOT NULL PRIMARY KEY,
3737
"altText" TEXT,
3838
"contentType" TEXT NOT NULL,
39-
"storageKey" TEXT NOT NULL,
39+
"objectKey" TEXT NOT NULL,
4040
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
4141
"updatedAt" DATETIME NOT NULL,
4242
"userId" TEXT NOT NULL,
@@ -189,7 +189,6 @@ CREATE UNIQUE INDEX "_RoleToUser_AB_unique" ON "_RoleToUser"("A", "B");
189189
CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B");
190190

191191

192-
193192
--------------------------------- Manual Seeding --------------------------
194193
-- Hey there, Kent here! This is how you can reliably seed your database with
195194
-- some data. You edit the migration.sql file and that will handle it for you.

prisma/schema.prisma

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ model NoteImage {
5151
id String @id @default(cuid())
5252
altText String?
5353
contentType String
54-
storageKey String
54+
objectKey String
5555
5656
createdAt DateTime @default(now())
5757
updatedAt DateTime @updatedAt
@@ -67,7 +67,7 @@ model UserImage {
6767
id String @id @default(cuid())
6868
altText String?
6969
contentType String
70-
storageKey String
70+
objectKey String
7171
7272
createdAt DateTime @default(now())
7373
updatedAt DateTime @updatedAt

0 commit comments

Comments
 (0)