LogoNEXTDEVKIT Docs

Storage

Learn how to handle file uploads and storage with S3-compatible services in NEXTDEVKIT

NEXTDEVKIT includes a robust storage system using S3-compatible services like AWS S3, Cloudflare R2, and others, providing secure file uploads, signed URLs, and flexible storage management.

🏗️ Storage System Architecture

NEXTDEVKIT's storage system is structured as follows:

src/
├── storage/
│   ├── actions.ts              # Server actions for storage
│   ├── types.ts                # Storage interfaces
│   └── providers/
│       ├── index.ts            # Provider registry
│       └── s3.ts               # S3-compatible provider
├── config/
│   └── index.ts                # Storage configuration

⚙️ Storage Configuration

App Configuration

Storage settings are configured in src/config/index.ts:

export const appConfig = {
  storage: {
    provider: "s3",           // Storage provider (currently s3-compatible)
    bucketNames: {
      avatars: process.env.NEXT_PUBLIC_AVATARS_BUCKET_NAME || "avatars",
      // Add more buckets as needed
    },
  },
  // ... other config
} as const;

AWS S3 Setup

Environment Variables

STORAGE_REGION=your_region # e.g. us-east-1
STORAGE_ACCESS_KEY_ID=your_access_key_id
STORAGE_SECRET_ACCESS_KEY=your_secret_access_key

# Bucket Names (optional, can be configured in app config)
NEXT_PUBLIC_AVATARS_BUCKET_NAME=your-bucket-name

AWS S3 Bucket Setup

  1. Create AWS Account: Sign up at aws.amazon.com

  2. Create S3 Bucket:

    • Go to S3 Object Storage
    • Create a new bucket
    • Disable "Block all public access" if needed
  3. Configure Bucket Policy:

    • Go to Bucket Permissions
    • Add the following policy:
    {
     "Version": "2012-10-17",
     "Statement": [
         {
             "Effect": "Allow",
             "Principal": "*",
             "Action": "s3:*",
             "Resource": "arn:aws:s3:::your-bucket-name/*"
         }
       ]
     }
  4. Configure CORS:

    • Go to Bucket CORS
    • Add the following CORS configuration:
    [
     {
         "AllowedHeaders": [
             "*"
         ],
         "AllowedMethods": [
             "PUT",
             "GET",
             "HEAD",
             "POST"
         ],
         "AllowedOrigins": [
             "*"
         ],
         "ExposeHeaders": [],
         "MaxAgeSeconds": 3000
     }
    ] 
  5. Generate API Token:

    • Create API token with S3 permissions
    • Use as access key credentials

Cloudflare R2 Setup

Environment Variables

STORAGE_REGION=auto
STORAGE_ACCESS_KEY_ID=your_access_key_id
STORAGE_SECRET_ACCESS_KEY=your_secret_access_key
STORAGE_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com

# Bucket Names (optional, can be configured in app config)
NEXT_PUBLIC_AVATARS_BUCKET_NAME=your-bucket-name

Cloudflare R2 Bucket Setup

  1. Create Cloudflare Account: Sign up at cloudflare.com

  2. Create R2 Bucket:

    • Go to R2 Object Storage
    • Create a new bucket
    • Set the custom domain to your bucket
  3. Configure CORS:

    • Go to Bucket CORS
    • Add the following CORS configuration:
    [
       {
         "AllowedOrigins": [
           "*"
         ],
         "AllowedMethods": [
           "PUT",
           "GET",
           "HEAD"
         ],
         "AllowedHeaders": [
           "Content-Type" // must be set
         ],
         "ExposeHeaders": [],
         "MaxAgeSeconds": 3000
       }
     ]
  4. Create a new API Token:

    • Go to R2/API/Manage API Tokens
    • Click Create User API Token
    • Set permissions to Object Read & Write to the bucket
    • Create the API Token, get the Access Key ID and Secret Access Key

📤 File Upload System

Server Actions

NEXTDEVKIT provides server actions for file operations:

src/storage/actions.ts
import { getSignedUploadUrl } from "@/storage/actions";

// Get signed URL for file upload
const result = await getSignedUploadUrl({
  bucket: appConfig.storage.bucketNames.avatars,
  key: avatarKey,
  contentType: file.type,
});

Uploading a file client side

src/components/settings/account/change-avatar-form.tsx
const result = await getSignedUploadUrl({
  bucket: appConfig.storage.bucketNames.avatars,
  key: avatarKey,
  contentType: file.type,
});

if (!result.data) {
  throw new Error("Failed to get upload URL");
}

const uploadResponse = await fetch(result.data, {
  method: "PUT",
  body: file,
  headers: {
    "Content-Type": file.type,
  },
});

const { error: updateError } = await authClient.updateUser({
  image: avatarKey,
});

Get a signed URL to access stored files

To access the files, you need to create a signed URL for the file.

This can either be done on demand or if you are listing your file entries from the database which should have the file path attached to them, you can already create a signed URL for each file entry.

All you need to add to your server actions is to call the getSignedUrl function from the storage package.

src/storage/actions.ts
export const getSignedUrl = actionClient
	.inputSchema(z.object({
		bucket: z.string(),
		key: z.string(),
		expiresIn: z.number().optional().default(60 * 60),
	}))
	.outputSchema(z.string())
	.action(async ({ parsedInput: { bucket, key, expiresIn } }) => {
		const storageProvider = getStorageProvider();
		return await storageProvider.getSignedUrl({ bucket, key, expiresIn });
	});

If you to avoid creating signed URLs each time you can create a proxy endpoint that will automatically create a signed URL for the requested file. This proxy endpoint should also include additional logic like checking if the user has access to the file or adding a cache layer.

For example, if you want to access the avatar image, you can create a route that will redirect to the signed URL for the file.

src/app/image-proxy/[...path]/route.ts
if (bucket === appConfig.storage.bucketNames.avatars) {
  const signedUrl = await getStorageProvider().getSignedUrl({
    bucket,
    key: filePath,
    expiresIn: 60 * 60,
  });

  return NextResponse.redirect(signedUrl, {
    headers: { "Cache-Control": "max-age=3600" },
  });
}

🔧 Troubleshooting

Common Issues

Upload Failures:

  • Check CORS configuration
  • Verify signed URL expiration
  • Ensure proper file permissions
  • Check network connectivity

Access Denied:

  • Verify IAM permissions
  • Check bucket policies
  • Ensure proper authentication

Large File Uploads:

  • Use multipart upload for large files
  • Implement progress tracking
  • Set appropriate timeouts

🎯 Next Steps

Now that you understand the storage system, explore these related features: