feat: whyrating - initial project from turbostarter boilerplate

This commit is contained in:
Alejandro Gutiérrez
2026-02-04 01:54:52 +01:00
commit 5cdc07cd39
1618 changed files with 338230 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
---
title: Configuration
description: Learn how to configure storage in TurboStarter.
url: /docs/web/storage/configuration
---
# Configuration
Currently, TurboStarter supports all S3-compatible storage providers, including [AWS S3](https://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces), [Cloudflare R2](https://www.cloudflare.com/products/r2/), and [Supabase Storage](https://supabase.com/storage).
For a concrete example using Supabase Storage as an S3-compatible provider, see the [Supabase recipe](/docs/web/recipes/supabase#use-supabase-storage-as-s3-compatible-storage).
The setup process is straightforward - you just need to configure a few environment variables in both your local environment and hosting provider:
```dotenv
S3_REGION=
S3_BUCKET=
S3_ENDPOINT=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
```
Let's break down each required variable:
* `S3_REGION`: The [AWS region](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/) where your storage is located - defaults to `us-east-1`
* `S3_BUCKET`: The default name of your storage bucket - you can pass different for each request
* `S3_ENDPOINT`: The S3 [endpoint URL](https://docs.aws.amazon.com/general/latest/gr/s3.html) for your storage provider - defaults to `https://s3.amazonaws.com`
* `S3_ACCESS_KEY_ID`: Your storage provider's [access key ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)
* `S3_SECRET_ACCESS_KEY`: Your storage provider's [secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)
You can learn more about S3 service configuration in the [official AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) or your specific storage provider's documentation.

View File

@@ -0,0 +1,178 @@
---
title: Managing files
description: Learn how to manage files in TurboStarter.
url: /docs/web/storage/managing-files
---
# Managing files
Before you start managing files, make sure you have [configured storage](/docs/web/storage/configuration).
## Permissions
Most S3-compatible storage providers allow you to configure bucket permissions and access policies. It's crucial to properly set these up to secure your files and control who can access them.
Here are some key security recommendations:
* Keep your bucket private by default
* Use IAM roles and policies to manage access
* Enable server-side encryption for sensitive data
* Configure CORS settings appropriately for client-side uploads
* Regularly audit bucket permissions and access logs
Making your bucket public is strongly discouraged as it can expose sensitive data and lead to unauthorized access and unexpected costs from bandwidth usage.
For detailed guidance on configuring bucket policies and permissions, refer to your storage provider's documentation:
* [AWS S3 Security Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html)
* [DigitalOcean Spaces Security](https://docs.digitalocean.com/products/spaces/how-to/manage-access/)
* [Cloudflare R2 Security](https://developers.cloudflare.com/r2/api/s3/tokens/)
* [Supabase Storage Security](https://supabase.com/docs/guides/storage/security/access-control)
## Uploading files
As explained in the [overview](/docs/web/storage/overview), TurboStarter uses presigned URLs to upload files to your storage provider.
We prepared a special endpoint to generate presigned URLs for your uploads to use in your client-side code.
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/upload",
enforceAuth,
validate("query", getObjectUrlSchema),
async (c) => c.json(await getUploadUrl(c.req.valid("query"))),
);
```
<Callout title="Expiration time" type="warn">
The signed URL is only valid for a limited time and will work for anyone who has access to it during that period. Make sure to handle the URL securely and avoid exposing it to unauthorized users.
</Callout>
Then, you can use it to upload files to the generated presigned URL from your frontend code:
```tsx title="upload.tsx"
const upload = useMutation({
mutationFn: async (data: { file?: File }) => {
const extension = data.file?.type.split("/").pop();
const path = `files/${crypto.randomUUID()}.${extension}`;
const { url: uploadUrl } = await handle(api.storage.upload.$get)({
query: { path },
});
const response = await fetch(uploadUrl, {
method: "PUT",
body: data.file,
headers: {
"Content-Type": data.file?.type ?? "",
},
});
if (!response.ok) {
throw new Error("Failed to upload file!");
}
},
onError: (error) => {
toast.error(error.message});
},
onSuccess: async ({ publicUrl, oldImage }, _b, context) => {
toast.success("File uploaded!");
},
});
```
The code above demonstrates how to implement file uploads in your application:
1. First, we have a server-side endpoint (`storageRouter`) that generates presigned URLs for uploads. This endpoint:
* [Requires authentication](/docs/web/api/protected-routes) via `enforceAuth`
* Validates the request parameters using `validate`
* Returns a presigned URL for uploading
2. Then, in the frontend code (`upload.tsx`), we use React Query's `useMutation` hook to handle the upload process:
* Requests a presigned URL from the server
* Uploads the file directly to the storage provider using the presigned URL
* Handles success and error cases with toast notifications
This approach ensures secure file uploads while avoiding server bandwidth costs and function timeout issues.
### Public uploads
Although **it's not recommended** to use public uploads in production, you can use the same endpoint to generate presigned URLs for public uploads:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/upload",
validate("query", getObjectUrlSchema),
async (c) => c.json(await getUploadUrl(c.req.valid("query"))),
);
```
Just remove the `enforceAuth` middleware from the endpoint and keep rest of the logic the same.
## Displaying files
We provide dedicated endpoints for retrieving signed URLs specifically for displaying files. These URLs are time-limited to maintain security, so they cannot be used for permanent storage or long-term access:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/signed",
enforceAuth,
validate("query", getObjectUrlSchema),
async (c) => c.json(await getSignedUrl(c.req.valid("query"))),
);
```
This endpoint is perfect for displaying files that should only be accessible to authorized users for a limited time.
### Public files
For displaying files publicly (without authorization and time limitations), you can use the `/public` endpoint:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/public",
validate("query", getObjectUrlSchema),
async (c) => c.json(await getPublicUrl(c.req.valid("query"))),
);
```
This endpoint generates a public URL for the file that you can use to display in your application. Please ensure that your bucket policy allows public access to the files and verify that you're not exposing any sensitive information.
## Deleting files
Deleting files works almost the same way as uploading files. You just need to generate a presigned URL for deletion and then use it to remove the file:
```ts title="storage/router.ts"
export const storageRouter = new Hono().get(
"/delete",
validate("query", getObjectUrlSchema),
async (c) => c.json(await getDeleteUrl(c.req.valid("query"))),
);
```
Then, in the frontend code, we use React Query's `useMutation` hook to handle the deletion process:
```tsx title="delete.tsx"
const remove = useMutation({
mutationFn: async () => {
const path = file.split("/").pop();
if (!path) return;
const { url: deleteUrl } = await handle(api.storage.delete.$get)({
query: { path: `files/${path}` },
});
await fetch(deleteUrl, {
method: "DELETE",
});
},
onError: (error) => {
toast.error(error.message);
},
onSuccess: () => {
toast.success("File removed!");
},
});
```
Now that you understand how to manage files in TurboStarter, it's time to build something awesome! Try creating a file upload component, building a photo gallery, or implementing a document management system.

View File

@@ -0,0 +1,34 @@
---
title: Overview
description: Get started with storage in TurboStarter.
url: /docs/web/storage/overview
---
# Overview
With TurboStarter, you can easily upload and manage files (images, videos, documents, and more) in your application.
Currently, all S3-compatible storage providers are supported, including [AWS S3](https://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces), [Cloudflare R2](https://www.cloudflare.com/products/r2/), [Supabase Storage](https://supabase.com/storage), and others.
If you're using Supabase, you can follow the [Supabase recipe](/docs/web/recipes/supabase#use-supabase-storage-as-s3-compatible-storage) for a concrete example of configuring Supabase Storage as your S3-compatible backend.
## Uploading files
The most common approach to uploading files is to use client-side uploads. With client-side uploads, you avoid paying ingress/egress fees for transferring file binary data through your server.
Additionally, most hosting platforms like [Vercel](https://vercel.com/docs/functions/runtimes#size-limits) or [Netlify](https://answers.netlify.com/t/what-is-the-maximum-file-size-upload-limit-in-a-netlify-form-submission/108419) have limitations on file size and maximum serverless function execution time.
That's why TurboStarter utilizes the **presigned URLs** feature of storage providers to upload files. Instead of sending files to the serverless function, the client requests a time-limited presigned URL from the serverless function and then uploads the file directly to the storage provider.
<ThemedImage alt="Client side uploads" light="/images/docs/web/storage/light.png" dark="/images/docs/web/storage/dark.png" width={1294} height={654} zoomable />
1. Client **requests** a presigned URL from the serverless function.
2. Server parses the request, validates the payload, optionally saves the metadata, and **returns the presigned URL** to the client.
3. Client **uploads the file** to the presigned URL within the expiration time.
4. (Optional) Once the file is uploaded, the serverless function is notified about the upload event, and the file metadata is saved to the database.
<Callout>
This approach ensures that credentials remain secure, handles authorization and authentication properly, and avoids the limitations of serverless platforms.
</Callout>
The configuration and use of storage is straightforward and simple. We'll explore this in more detail in the following sections.