feat: turbostarter boilerplate

Production-ready Next.js boilerplate with:
- Runtime env validation (fail-fast on missing vars)
- Feature-gated config (S3, Stripe, email, OAuth)
- Docker + Coolify deployment pipeline
- PostgreSQL + pgvector, MinIO S3, Better Auth
- TypeScript strict mode (no ignoreBuildErrors)
- i18n (en/es), AI modules, billing, monitoring

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-02 17:29:12 +00:00
commit 3527e732d4
1618 changed files with 338230 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import baseConfig from "@turbostarter/eslint-config/base";
export default baseConfig;

View File

@@ -0,0 +1,31 @@
{
"name": "@turbostarter/storage",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./env": "./src/env.ts",
"./server": "./src/server.ts"
},
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@turbostarter/prettier-config",
"dependencies": {
"@aws-sdk/client-s3": "3.922.0",
"@aws-sdk/s3-request-presigner": "3.922.0",
"@turbostarter/shared": "workspace:*"
},
"devDependencies": {
"@turbostarter/eslint-config": "workspace:*",
"@turbostarter/prettier-config": "workspace:*",
"@turbostarter/tsconfig": "workspace:*",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -0,0 +1 @@
export * from "./providers/env";

View File

@@ -0,0 +1,13 @@
import * as z from "zod";
import { env } from "../env";
export const getObjectUrlSchema = z.object({
path: z.string(),
bucket: z
.string()
.optional()
.default(env.S3_BUCKET ?? ""),
});
export type GetObjectUrlInput = z.input<typeof getObjectUrlSchema>;

View File

@@ -0,0 +1 @@
export * from "./s3/env";

View File

@@ -0,0 +1 @@
export * from "./s3";

View File

@@ -0,0 +1,27 @@
import { S3Client } from "@aws-sdk/client-s3";
import { env } from "../../env";
let s3Client: S3Client | null = null;
export const getClient = () => {
if (s3Client) {
return s3Client;
}
if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {
throw new Error("S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY are required when using S3 storage");
}
s3Client = new S3Client({
forcePathStyle: true,
region: env.S3_REGION,
endpoint: env.S3_ENDPOINT,
credentials: {
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
},
});
return s3Client;
};

View File

@@ -0,0 +1,22 @@
import { defineEnv } from "envin";
import * as z from "zod";
import { envConfig } from "@turbostarter/shared/constants";
import type { Preset } from "envin/types";
export const preset = {
id: "s3",
server: {
S3_BUCKET: z.string().optional(),
S3_REGION: z.string().optional().default("us-east-1"),
S3_ENDPOINT: z.string().optional(),
S3_ACCESS_KEY_ID: z.string().optional(),
S3_SECRET_ACCESS_KEY: z.string().optional(),
},
} as const satisfies Preset;
export const env = defineEnv({
...envConfig,
...preset,
});

View File

@@ -0,0 +1,94 @@
import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl as getSignedUrlCommand } from "@aws-sdk/s3-request-presigner";
import { getObjectUrlSchema } from "../../lib/schema";
import { getClient } from "./client";
import type { GetObjectUrlInput } from "../../lib/schema";
import type { StorageProviderStrategy } from "../types";
// Helper to apply schema defaults (bucket from env)
const withDefaults = (input: GetObjectUrlInput) => getObjectUrlSchema.parse(input);
export const { getUploadUrl, getSignedUrl, getPublicUrl, getDeleteUrl } = {
getUploadUrl: async (input: GetObjectUrlInput) => {
const { path, bucket } = withDefaults(input);
const client = getClient();
const url = await getSignedUrlCommand(
client,
new PutObjectCommand({
Bucket: bucket,
Key: path,
}),
{
expiresIn: 60,
},
);
return { url };
},
getSignedUrl: async (input: GetObjectUrlInput) => {
const { path, bucket } = withDefaults(input);
const client = getClient();
const url = await getSignedUrlCommand(
client,
new GetObjectCommand({
Bucket: bucket,
Key: path,
}),
{
expiresIn: 3600,
},
);
return { url };
},
getPublicUrl: async (input: GetObjectUrlInput) => {
const { path, bucket } = withDefaults(input);
const client = getClient();
const endpoint = await client.config.endpoint?.();
const forcePathStyle = client.config.forcePathStyle;
if (endpoint?.hostname.includes("supabase.co")) {
return {
url: `${endpoint.protocol}//${endpoint.hostname}/storage/v1/object/public/${bucket}/${path}`,
};
}
// Use path-style URL for MinIO and other S3-compatible storage (forcePathStyle: true)
if (forcePathStyle) {
const port = endpoint?.port ? `:${endpoint.port}` : "";
return {
url: `${endpoint?.protocol}//${endpoint?.hostname}${port}/${bucket}/${path}`,
};
}
return {
url: `${endpoint?.protocol}//${bucket}.${endpoint?.hostname}/${path}`,
};
},
getDeleteUrl: async (input: GetObjectUrlInput) => {
const { path, bucket } = withDefaults(input);
const client = getClient();
const url = await getSignedUrlCommand(
client,
new DeleteObjectCommand({
Bucket: bucket,
Key: path,
}),
{
expiresIn: 60,
},
);
return { url };
},
} satisfies StorageProviderStrategy;

View File

@@ -0,0 +1,8 @@
import type { GetObjectUrlInput } from "../lib/schema";
export interface StorageProviderStrategy {
getUploadUrl: (data: GetObjectUrlInput) => Promise<{ url: string }>;
getSignedUrl: (data: GetObjectUrlInput) => Promise<{ url: string }>;
getPublicUrl: (data: GetObjectUrlInput) => Promise<{ url: string }>;
getDeleteUrl: (data: GetObjectUrlInput) => Promise<{ url: string }>;
}

View File

@@ -0,0 +1,8 @@
export {
getUploadUrl,
getDeleteUrl,
getPublicUrl,
getSignedUrl,
} from "./providers";
export * from "./lib/schema";

View File

@@ -0,0 +1,6 @@
{
"extends": "@turbostarter/tsconfig/internal.json",
"compilerOptions": {},
"include": ["*.ts", "src/**/*"],
"exclude": ["node_modules"]
}