├── services ├── trpc-api │ ├── src │ │ ├── index.ts │ │ ├── routers │ │ │ ├── index.ts │ │ │ └── todos │ │ │ │ ├── create.ts │ │ │ │ └── list.ts │ │ ├── middlewares │ │ │ └── user-info.ts │ │ ├── trpc.ts │ │ └── handler.ts │ ├── sst-env.d.ts │ ├── tsconfig.json │ ├── test │ │ └── addTodo.test.ts │ └── package.json ├── add-todo │ ├── sst-env.d.ts │ ├── tsconfig.json │ ├── src │ │ ├── types.ts │ │ └── addTodo │ │ │ └── index.ts │ ├── package.json │ └── test │ │ └── addTodo.test.ts └── frontend │ ├── sst-env.d.ts │ ├── src │ ├── components │ │ └── Hello │ │ │ └── index.tsx │ ├── helpers │ │ └── trpc.ts │ └── pages │ │ ├── _document.tsx │ │ ├── _app.tsx │ │ └── index.tsx │ ├── postcss.config.cjs │ ├── public │ ├── favicon.ico │ ├── vercel.svg │ └── next.svg │ ├── .eslintrc.json │ ├── next-env.d.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── styles │ └── globals.css │ ├── package.json │ ├── README.md │ └── next.config.js ├── packages └── timestamp │ ├── sst-env.d.ts │ ├── src │ └── index.ts │ ├── tsconfig.json │ ├── package.json │ └── test │ └── index.test.ts ├── .husky ├── pre-commit └── commit-msg ├── .releaserc ├── commitlint.config.cjs ├── .env-example ├── tsconfig.json ├── .eslintignore ├── lint-staged.config.js ├── pnpm-workspace.yaml ├── .npmrc ├── .editorconfig ├── prettier.config.cjs ├── .vscode ├── extensions.json └── settings.json ├── .github ├── pull_request_template.md └── workflows │ ├── deploy.yaml │ ├── remove.yaml │ ├── release-prod.yaml │ └── test.yaml ├── constructs └── ExampleConstruct.ts ├── .gitignore ├── tsconfig.base.json ├── vitest.config.ts ├── stacks ├── resources │ └── stack.ts ├── add-todo │ ├── stack.ts │ └── AddTodoStateMachine.ts ├── utils.ts ├── trpc-api │ └── stack.ts └── frontend │ └── index.ts ├── LICENSE ├── sst.config.ts ├── .eslintrc.cjs ├── package.json └── README.md /services/trpc-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { AppRouter } from './routers' 2 | -------------------------------------------------------------------------------- /packages/timestamp/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /services/add-todo/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /services/frontend/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /services/trpc-api/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "release": { 3 | "branches": [ 4 | "master" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | AWS_SDK_LOAD_CONFIG=true 2 | AWS_PROFILE=your-aws-profile 3 | AWS_REGION=eu-central-1 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "exclude": ["services", "packages"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .next 4 | .open-next 5 | .sst 6 | 7 | package-lock.json 8 | 9 | -------------------------------------------------------------------------------- /services/frontend/src/components/Hello/index.tsx: -------------------------------------------------------------------------------- 1 | export const Hello: React.FC = () => { 2 | return
hello
3 | } 4 | -------------------------------------------------------------------------------- /services/frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /services/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purple-technology/purple-stack/HEAD/services/frontend/public/favicon.ico -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '*.{ts,tsx,js,jsx,cjs,mjs}': 'eslint --fix', 3 | '*.{html,json}': 'prettier --write' 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "services/**/*" 3 | - "packages/**/*" 4 | 5 | catalog: 6 | typescript: ^5.5.4 7 | "@types/node": ^20.12.7 8 | -------------------------------------------------------------------------------- /packages/timestamp/src/index.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export const getTimestamp = (): string => 4 | moment().format('YYYY-MM-DD HH:mm:ss.SSS') 5 | -------------------------------------------------------------------------------- /services/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "settings": { 4 | "next": { 5 | "rootDir": "services/frontend" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/timestamp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@package/*": ["../*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/add-todo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@package/*": ["../*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/trpc-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@package/*": ["../*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @purple-technology:registry=https://npm.pkg.github.com 2 | use-node-version=20.11.0 3 | node-version=20.11.0 4 | engine-strict=true 5 | manage-package-manager-versions=true 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,jsx,json,ts,tsx,cjs,mjs}] 8 | charset = utf-8 9 | indent_style = tab 10 | indent_size=2 11 | -------------------------------------------------------------------------------- /services/frontend/src/helpers/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from '@trpc/react-query' 2 | import type { AppRouter } from '@trpc-api/index' 3 | 4 | export const trpc = createTRPCReact() 5 | 6 | export { AppRouter } 7 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | useTabs: true, 5 | trailingComma: 'none', 6 | plugins: ['prettier-plugin-tailwindcss'], 7 | tailwindConfig: './services/frontend/tailwind.config.ts' 8 | } 9 | -------------------------------------------------------------------------------- /services/frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /services/add-todo/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const schema = z.object({ 4 | text: z.string().min(1) 5 | }) 6 | 7 | export type Input = z.infer 8 | 9 | export type Output = { 10 | success: boolean 11 | } 12 | -------------------------------------------------------------------------------- /services/trpc-api/test/addTodo.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from 'vitest' 2 | 3 | describe('addTodo', () => { 4 | beforeEach(() => { 5 | vi.resetAllMocks() 6 | }) 7 | 8 | it('should succeed', async () => { 9 | expect(1).toEqual(1) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "eamodio.gitlens", 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "vivaxy.vscode-conventional-commits", 8 | "SebastianBille.iam-legend", 9 | "bradlc.vscode-tailwindcss" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /services/frontend/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document' 2 | 3 | const Document: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default Document 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## I have created this PR because 2 | 3 | 11 | -------------------------------------------------------------------------------- /packages/timestamp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@purple-stack-packages/timestamp", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "typecheck": "tsc -noEmit" 8 | }, 9 | "devDependencies": { 10 | "vitest": "^1.2.1" 11 | }, 12 | "dependencies": { 13 | "moment": "^2.30.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /constructs/ExampleConstruct.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs' 2 | 3 | export interface ExampleConstructProps {} 4 | 5 | export class ExampleConstruct extends Construct { 6 | constructor(scope: Construct, id: string, props: ExampleConstructProps) { 7 | super(scope, id) 8 | // here goes your construct code 9 | // make sure to use "this" when providing "scope" to child constructs 10 | console.log(props) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/trpc-api/src/routers/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '../trpc' 2 | import { createTodos } from './todos/create' 3 | import { listTodos } from './todos/list' 4 | 5 | export const appRouter = t.router({ 6 | todos: t.router({ 7 | create: createTodos, 8 | list: listTodos 9 | }) 10 | }) 11 | 12 | // Export only the type of a router! 13 | // This prevents us from importing server code on the client. 14 | export type AppRouter = typeof appRouter 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # debug 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | 6 | 7 | *.tsbuildinfo 8 | node_modules 9 | pnpm-exec-summary.json 10 | 11 | # Coverage directory used by jest 12 | coverage 13 | coverage-report 14 | .nyc_output 15 | 16 | # environment configuration file 17 | .env 18 | 19 | # sst 20 | .sst 21 | .build 22 | 23 | # opennext 24 | .next 25 | out 26 | .open-next 27 | 28 | # misc 29 | .DS_Store 30 | 31 | # CDK 32 | cdk.context.json 33 | -------------------------------------------------------------------------------- /services/add-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@services/add-todo", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "vitest run", 7 | "test:watch": "vitest", 8 | "typecheck": "tsc -noEmit" 9 | }, 10 | "devDependencies": { 11 | "vitest": "^1.2.1", 12 | "@aws-sdk/client-dynamodb": "^3.499.0", 13 | "@types/aws-lambda": "^8.10.131", 14 | "aws-sdk-client-mock": "^3.0.1", 15 | "typescript": "catalog:" 16 | }, 17 | "dependencies": { 18 | "zod": "^3.22.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/timestamp/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from 'vitest' 2 | 3 | import { getTimestamp } from '../src/index' 4 | 5 | vi.mock('moment', () => ({ 6 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 7 | default: () => ({ 8 | format: (): string => '2024-01-01 01:01:01.001' 9 | }) 10 | })) 11 | 12 | describe('addTodo', () => { 13 | beforeEach(() => { 14 | vi.resetAllMocks() 15 | }) 16 | 17 | it('should succeed', async () => { 18 | expect(getTimestamp()).toEqual('2024-01-01 01:01:01.001') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /services/frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}' 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' 15 | } 16 | } 17 | }, 18 | plugins: [] 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "alwaysStrict": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "useUnknownInCatchVariables": true, 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "noErrorTruncation": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/trpc-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@services/trpc-api", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "vitest run", 7 | "test:watch": "vitest", 8 | "typecheck": "tsc -noEmit" 9 | }, 10 | "devDependencies": { 11 | "vitest": "^1.2.1", 12 | "@aws-sdk/client-dynamodb": "^3.499.0", 13 | "@types/aws-lambda": "^8.10.131", 14 | "aws-sdk-client-mock": "^3.0.1" 15 | }, 16 | "dependencies": { 17 | "@aws-lambda-powertools/logger": "^1.17.0", 18 | "@trpc/server": "10.45.1", 19 | "superjson": "^1.13.1", 20 | "zod": "^3.22.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/trpc-api/src/routers/todos/create.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger' 2 | import { z } from 'zod' 3 | 4 | import { userInfo } from '../../middlewares/user-info' 5 | import { publicProcedure } from '../../trpc' 6 | 7 | const logger = new Logger({ serviceName: 'trpc-api-todos-create' }) 8 | 9 | export const createTodos = publicProcedure 10 | .use(userInfo) 11 | .input( 12 | z.object({ 13 | name: z.string() 14 | }) 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | logger.info('created', { ctx, input }) 18 | 19 | return { 20 | created: true, 21 | input, 22 | ctx 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /services/frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/trpc-api/src/routers/todos/list.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger' 2 | import { z } from 'zod' 3 | 4 | import { userInfo } from '../../middlewares/user-info' 5 | import { publicProcedure } from '../../trpc' 6 | 7 | const logger = new Logger({ serviceName: 'trpc-api-todos-list' }) 8 | 9 | export const listTodos = publicProcedure 10 | .use(userInfo) 11 | .input( 12 | z.object({ 13 | id: z.string() 14 | }) 15 | ) 16 | .query(async ({ ctx, input }) => { 17 | logger.info('list', { ctx, input }) 18 | 19 | return [ 20 | { 21 | name: 'a' 22 | }, 23 | { 24 | name: 'b' 25 | } 26 | ] 27 | }) 28 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig(() => { 6 | return { 7 | test: { 8 | testTimeout: 30000, 9 | alias: { 10 | '@packages/': new URL('./packages/', import.meta.url).pathname 11 | }, 12 | coverage: { 13 | cleanOnRerun: true, 14 | enabled: true, 15 | clean: true, 16 | all: true, 17 | exclude: [ 18 | 'services/frontend', 19 | 'stacks', 20 | 'constructs', 21 | '**/*.d.ts', 22 | 'sst.config.ts', 23 | '.sst' 24 | ], 25 | extension: ['.ts'] 26 | // 100: true 27 | } 28 | } 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /services/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "paths": { 17 | "@/*": ["./src/*"], 18 | "@services/trpc-api/*": ["../trpc-api/src"], 19 | "@package/*": ["../../packages/*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /services/frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /services/trpc-api/src/middlewares/user-info.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from '@trpc/server' 2 | 3 | import { t } from '../trpc' 4 | 5 | const someDatabaseCall = ( 6 | userCognitoId: string 7 | ): 8 | | { userCognitoId: string; name: string; balance: number; currency: string } 9 | | undefined => { 10 | return { 11 | userCognitoId, 12 | name: 'Karel', 13 | balance: 1000, 14 | currency: 'JPY' 15 | } 16 | } 17 | 18 | export const userInfo = t.middleware(async (params) => { 19 | const user = someDatabaseCall(params.ctx.userCognitoId) 20 | 21 | if (typeof user === 'undefined') { 22 | throw new TRPCError({ 23 | code: 'BAD_REQUEST', 24 | message: 'User not found.' 25 | }) 26 | } 27 | 28 | return params.next({ 29 | ctx: { 30 | ...params.ctx, 31 | user 32 | } 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /stacks/resources/stack.ts: -------------------------------------------------------------------------------- 1 | import type { StackContext } from 'sst/constructs' 2 | import { Table } from 'sst/constructs' 3 | 4 | import { tableRemovalPolicy } from '../utils' 5 | 6 | interface ResourcesStackOutput { 7 | todosTable: Table 8 | } 9 | 10 | export function ResourcesStack({ 11 | stack, 12 | app 13 | }: StackContext): ResourcesStackOutput { 14 | /* 15 | * 16 | * Once you add some new resource to this stack, remove the contents below 17 | * 18 | */ 19 | 20 | const todosTable = new Table(stack, 'TodosTable', { 21 | fields: { 22 | createdTimestamp: 'string' 23 | }, 24 | primaryIndex: { 25 | partitionKey: 'createdTimestamp' 26 | }, 27 | cdk: { 28 | table: { 29 | ...tableRemovalPolicy(app) 30 | } 31 | } 32 | }) 33 | 34 | return { 35 | todosTable 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stacks/add-todo/stack.ts: -------------------------------------------------------------------------------- 1 | import type { IStateMachine } from 'aws-cdk-lib/aws-stepfunctions' 2 | import type { StackContext } from 'sst/constructs' 3 | import { use } from 'sst/constructs' 4 | 5 | import { ResourcesStack } from '../resources/stack' 6 | import { AddTodoStateMachine } from './AddTodoStateMachine' 7 | 8 | interface AddTodoStackOutput { 9 | addTodoStateMachine: IStateMachine 10 | } 11 | 12 | export function AddTodoStack({ stack }: StackContext): AddTodoStackOutput { 13 | const { todosTable } = use(ResourcesStack) 14 | 15 | stack.setDefaultFunctionProps({ 16 | bind: [todosTable] 17 | }) 18 | 19 | const addTodoStateMachine = new AddTodoStateMachine( 20 | stack, 21 | 'AddTodoStateMachine', 22 | { 23 | servicePath: 'services/add-todo/src' 24 | } 25 | ) 26 | 27 | return { 28 | addTodoStateMachine: addTodoStateMachine.stateMachine 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /services/add-todo/src/addTodo/index.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb' 2 | import { getTimestamp } from '@packages/timestamp' 3 | import { Table } from 'sst/node/table' 4 | 5 | import type { Output } from '../types' 6 | import { schema } from '../types' 7 | 8 | const client = new DynamoDBClient({}) 9 | 10 | export const handler = async (_input: unknown): Promise => { 11 | const x = schema.safeParse(_input) 12 | 13 | if (!x.success) { 14 | throw new Error('Invalid input') 15 | } 16 | 17 | const input = x.data 18 | 19 | await client.send( 20 | new PutItemCommand({ 21 | TableName: Table.TodosTable.tableName, 22 | Item: { 23 | createdTimestamp: { 24 | S: getTimestamp() 25 | }, 26 | text: { 27 | S: input.text 28 | }, 29 | checked: { 30 | BOOL: false 31 | } 32 | } 33 | }) 34 | ) 35 | 36 | return { 37 | success: true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /services/add-todo/test/addTodo.test.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb' 2 | import { mockClient } from 'aws-sdk-client-mock' 3 | import { beforeEach, describe, expect, it, vi } from 'vitest' 4 | 5 | import { handler as addTodo } from '../src/addTodo' 6 | 7 | const ddbMock = mockClient(DynamoDBClient) 8 | 9 | vi.mock('sst/node/table', () => ({ 10 | Table: { 11 | TodosTable: { 12 | tableName: '' 13 | } 14 | } 15 | })) 16 | 17 | vi.mock('@packages/timestamp', () => ({ 18 | getTimestamp: (): string => '2024-01-01 00:00:00.000' 19 | })) 20 | 21 | describe('addTodo', () => { 22 | beforeEach(() => { 23 | ddbMock.reset() 24 | vi.resetAllMocks() 25 | }) 26 | 27 | it('should succeed', async () => { 28 | ddbMock.on(PutItemCommand).resolves({}) 29 | 30 | expect( 31 | await addTodo({ 32 | text: 'karel' 33 | }) 34 | ).toEqual({ 35 | success: true 36 | }) 37 | }) 38 | 39 | it('should fail when invalid input', async () => { 40 | ddbMock.on(PutItemCommand).resolves({}) 41 | 42 | expect(addTodo({})).rejects.toThrow() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Purple Technology s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@services/frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "env": "env-cmd -f ../../.env bash -c \"aws sts get-caller-identity --profile \\$AWS_PROFILE --no-cli-pager >/dev/null 2>&1 || aws sso login --profile \\$AWS_PROFILE\" && env-cmd -f ../../.env", 8 | "dev": "pnpm run env -- sst bind \"IS_LOCAL=true next dev\"", 9 | "dev:staging": "pnpm run env -- sst bind --stage=staging \"IS_LOCAL=true next dev\"", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint", 13 | "typecheck": "tsc -noEmit" 14 | }, 15 | "dependencies": { 16 | "@tanstack/react-query": "^4.36.1", 17 | "@trpc/client": "^10.45.1", 18 | "@trpc/react-query": "^10.45.1", 19 | "next": "14.1.0", 20 | "react": "^18", 21 | "react-dom": "^18", 22 | "superjson": "^1.13.1" 23 | }, 24 | "devDependencies": { 25 | "env-cmd": "^10.1.0", 26 | "@types/node": "catalog:", 27 | "@types/react": "^18", 28 | "@types/react-dom": "^18", 29 | "autoprefixer": "^10.0.1", 30 | "eslint-config-next": "14.1.0", 31 | "postcss": "^8", 32 | "tailwindcss": "^3.3.0", 33 | "typescript": "catalog:" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Branch 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | - staging 8 | permissions: 9 | id-token: write # This is required for requesting the JWT 10 | contents: read # This is required for actions/checkout 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v3 20 | with: 21 | version: 9 22 | 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | cache-dependency-path: pnpm-lock.yaml 29 | 30 | - name: Install Dependencies 31 | run: pnpm install 32 | 33 | - name: Configure AWS credentials 34 | uses: aws-actions/configure-aws-credentials@v4 35 | with: 36 | role-to-assume: ${{ github.ref == 'refs/heads/master' && 'CHANGE_ME' || 'CHANGE_ME' }} 37 | aws-region: eu-central-1 # Is not related to deployment region 38 | role-session-name: CHANGE_ME 39 | 40 | - name: Deploy 41 | run: pnpm sst deploy 42 | -------------------------------------------------------------------------------- /services/trpc-api/src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger' 2 | import { initTRPC } from '@trpc/server' 3 | import superjson from 'superjson' 4 | 5 | export interface Context { 6 | readonly userCognitoId: string 7 | } 8 | 9 | const logger = new Logger({ serviceName: 'trpc-api' }) 10 | 11 | export const t = initTRPC.context().create({ 12 | transformer: superjson, 13 | errorFormatter: (options) => { 14 | const getMessage = (): string => { 15 | switch (options.error.code) { 16 | case 'INTERNAL_SERVER_ERROR': 17 | case 'BAD_REQUEST': 18 | return options.error.message ?? 'Something went wrong.' 19 | default: 20 | return options.shape.message 21 | } 22 | } 23 | 24 | return { 25 | ...options.shape, 26 | message: getMessage() 27 | } 28 | } 29 | }) 30 | 31 | export const profiler = t.middleware(async (params) => { 32 | const startTime = Date.now() 33 | 34 | const result = await params.next() 35 | 36 | logger.debug('Procedure execution time', { 37 | path: params.path, 38 | executionTime: Date.now() - startTime 39 | }) 40 | 41 | return result 42 | }) 43 | 44 | export const publicProcedure = t.procedure.use(profiler) 45 | 46 | export const router = t.router 47 | export const middleware = t.middleware 48 | -------------------------------------------------------------------------------- /.github/workflows/remove.yaml: -------------------------------------------------------------------------------- 1 | name: Remove Deployment 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | id-token: write # This is required for requesting the JWT 6 | contents: read # This is required for actions/checkout 7 | jobs: 8 | deploy: 9 | # We don't want to remove protected deployment 10 | if: github.ref_protected == false 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v3 18 | with: 19 | version: 9 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: pnpm 26 | cache-dependency-path: pnpm-lock.yaml 27 | 28 | - name: Install Dependencies 29 | run: pnpm install 30 | 31 | - name: Configure AWS credentials 32 | uses: aws-actions/configure-aws-credentials@v4 33 | with: 34 | role-to-assume: ${{ github.ref == 'refs/heads/master' && 'CHANGE_ME' || 'CHANGE_ME' }} 35 | aws-region: eu-central-1 # Is not related to deployment region 36 | role-session-name: CHANGE_ME 37 | 38 | - name: Remove 39 | run: pnpm sst remove 40 | -------------------------------------------------------------------------------- /stacks/utils.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy } from 'aws-cdk-lib' 2 | import type { TableProps } from 'aws-cdk-lib/aws-dynamodb' 3 | import type { QueueProps } from 'aws-cdk-lib/aws-sqs' 4 | import type { App } from 'sst/constructs' 5 | 6 | export const isProd = (app: App): boolean => app.stage === 'master' 7 | 8 | export const isStaging = (app: App): boolean => app.stage === 'staging' 9 | 10 | export const appDomain = (app: App): string => 11 | isProd(app) 12 | ? 'www.purple-technology.com' 13 | : app.local 14 | ? 'localhost:3000' 15 | : `${app.stage.toLowerCase()}-app-ksnxaq.staging.purple-technology.com` 16 | 17 | export const appUrl = (app: App): string => 18 | `${app.local ? 'http' : 'https'}://${appDomain(app)}` 19 | 20 | export const getRemovalPolicy = (app: App): RemovalPolicy => 21 | isProd(app) ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY 22 | 23 | export const tableRemovalPolicy = ( 24 | app: App 25 | ): Pick => ({ 26 | deletionProtection: isProd(app), 27 | removalPolicy: isProd(app) ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY 28 | }) 29 | 30 | export const queueRemovalPolicy = ( 31 | app: App 32 | ): Pick => ({ 33 | removalPolicy: isProd(app) ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY 34 | }) 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[html]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[json]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[jsonc]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 13 | }, 14 | "[javascriptreact]": { 15 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 16 | }, 17 | "[javascript]": { 18 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 19 | }, 20 | "[typescript]": { 21 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 22 | }, 23 | "[css]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode" 25 | }, 26 | "files.associations": { 27 | "*.env": "ini" 28 | }, 29 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 30 | "eslint.format.enable": true, 31 | "editor.formatOnSave": true, 32 | "prettier.ignorePath": ".eslintignore", 33 | "prettier.useEditorConfig": true, 34 | "typescript.tsdk": "node_modules/typescript/lib", 35 | "search.exclude": { 36 | "**/.sst": true, 37 | "**/.next": true, 38 | "**/.open-next": true 39 | }, 40 | "npm.packageManager": "pnpm", 41 | // Due to custom next.js eslint settings 42 | "eslint.workingDirectories": ["/**/*", "services/frontend"] 43 | } 44 | -------------------------------------------------------------------------------- /services/frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/trpc-api/src/handler.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger' 2 | import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda' 3 | import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda' 4 | import type { 5 | APIGatewayProxyWithCognitoAuthorizerEvent, 6 | APIGatewayProxyWithCognitoAuthorizerHandler 7 | } from 'aws-lambda' 8 | 9 | import { appRouter } from './routers' 10 | import type { Context } from './trpc' 11 | 12 | const createContext = ( 13 | { 14 | // event 15 | }: CreateAWSLambdaContextOptions 16 | ): Context => { 17 | return { 18 | /* In case you want to use Cognito */ 19 | // userCognitoId: event.requestContext.authorizer.claims['sub'] as string 20 | userCognitoId: 'karel123' 21 | } 22 | } 23 | 24 | const logger = new Logger({ serviceName: 'trpc-api' }) 25 | 26 | export const handler: APIGatewayProxyWithCognitoAuthorizerHandler = ( 27 | event, 28 | context 29 | ) => 30 | awsLambdaRequestHandler({ 31 | router: appRouter, 32 | responseMeta: () => ({ 33 | headers: { 34 | 'Access-Control-Allow-Origin': `${ 35 | (process.env['ALLOW_ORIGINS'] ?? '') 36 | .split(';') 37 | .includes(event.headers['origin'] ?? 'none') 38 | ? event.headers['origin'] 39 | : process.env['APP_URL'] 40 | }` 41 | } 42 | }), 43 | onError({ error }) { 44 | logger.error('tRPC Error', error) 45 | }, 46 | createContext 47 | })(event, context) 48 | -------------------------------------------------------------------------------- /sst.config.ts: -------------------------------------------------------------------------------- 1 | import { getStage } from '@purple/serverless-git-branch-stage-plugin' 2 | import type { SSTConfig } from 'sst' 3 | 4 | import { AddTodoStack } from './stacks/add-todo/stack' 5 | import { FrontendStack } from './stacks/frontend' 6 | import { ResourcesStack } from './stacks/resources/stack' 7 | import { TrpcApiStack } from './stacks/trpc-api/stack' 8 | import { isProd, isStaging } from './stacks/utils' 9 | 10 | const config: SSTConfig = { 11 | config(globals) { 12 | const stage = globals.stage ?? getStage() 13 | 14 | return { 15 | name: 'purple-stack', 16 | stage, 17 | region: stage === 'master' ? 'eu-central-1' : 'eu-central-1' 18 | } 19 | }, 20 | stacks(app) { 21 | if (app.stage === 'master' && app.mode === 'dev') { 22 | throw new Error('Cannot deploy master stage in dev mode') 23 | } 24 | 25 | app.setDefaultFunctionProps({ 26 | runtime: 'nodejs20.x', 27 | architecture: 'arm_64', 28 | logRetention: 'three_months', 29 | logRetentionRetryOptions: { maxRetries: 100 }, 30 | tracing: 'disabled', 31 | environment: { 32 | // https://docs.powertools.aws.dev/lambda/typescript/latest/#environment-variables 33 | POWERTOOLS_DEV: app.local ? 'true' : 'false', 34 | POWERTOOLS_LOG_LEVEL: app.local 35 | ? 'DEBUG' 36 | : isProd(app) || isStaging(app) 37 | ? 'WARN' 38 | : 'INFO' 39 | } 40 | }) 41 | 42 | app 43 | .stack(ResourcesStack) 44 | .stack(AddTodoStack) 45 | .stack(TrpcApiStack) 46 | .stack(FrontendStack) 47 | } 48 | } 49 | 50 | export default config 51 | -------------------------------------------------------------------------------- /stacks/trpc-api/stack.ts: -------------------------------------------------------------------------------- 1 | import { Cors } from 'aws-cdk-lib/aws-apigateway' 2 | import type { StackContext } from 'sst/constructs' 3 | import { ApiGatewayV1Api } from 'sst/constructs' 4 | 5 | import { appUrl, isProd } from '../utils' 6 | 7 | interface TrpcApiStackOutput { 8 | restApi: ApiGatewayV1Api<{ 9 | /* In case you want to use Cognito */ 10 | // AppCognitoAuth: { 11 | // type: 'user_pools' 12 | // userPoolIds: string[] 13 | // identitySource: string 14 | // } 15 | }> 16 | } 17 | 18 | export function TrpcApiStack({ stack, app }: StackContext): TrpcApiStackOutput { 19 | const allowOrigins = [ 20 | appUrl(app), 21 | ...(isProd(app) ? [] : ['http://localhost:3000']) 22 | ] 23 | 24 | const api = new ApiGatewayV1Api(stack, 'TrpcApi', { 25 | cdk: { 26 | restApi: { 27 | defaultCorsPreflightOptions: { 28 | allowOrigins, 29 | allowMethods: Cors.ALL_METHODS, 30 | allowCredentials: true 31 | } 32 | } 33 | }, 34 | /* In case you want to use Cognito */ 35 | // authorizers: { 36 | // AppCognitoAuth: { 37 | // type: 'user_pools', 38 | // userPoolIds: [resources.userPool.userPoolId], 39 | // identitySource: 'method.request.header.Authorization' 40 | // } 41 | // }, 42 | routes: { 43 | 'ANY /trpc/{proxy+}': { 44 | /* In case you want to use Cognito */ 45 | // authorizer: 'App CognitoAuth', 46 | function: { 47 | handler: './services/trpc-api/src/handler.handler', 48 | environment: { 49 | APP_URL: appUrl(app), 50 | ALLOW_ORIGINS: allowOrigins.join(';') 51 | } 52 | } 53 | } 54 | } 55 | }) 56 | 57 | stack.addOutputs({ 58 | apiUrl: api.url 59 | }) 60 | 61 | return { 62 | restApi: api 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /services/frontend/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../../styles/globals.css' 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 4 | import { httpBatchLink } from '@trpc/client' 5 | import type { AppProps } from 'next/app' 6 | import Head from 'next/head' 7 | import { useState } from 'react' 8 | import superjson from 'superjson' 9 | 10 | import { trpc } from '@/helpers/trpc' 11 | 12 | const App: React.FC = ({ Component, pageProps }) => { 13 | /* In case you want to use Cognito */ 14 | // const token = useRef() 15 | 16 | /* In case you want to use Cognito */ 17 | // useEffect(() => { 18 | // token.current = cognito_tokens_goes_here.idToken 19 | // }, [cognito_token_goes_here?.idToken]) 20 | 21 | const [queryClient] = useState( 22 | () => 23 | new QueryClient({ 24 | defaultOptions: { 25 | queries: { 26 | // https://medium.com/doctolib/react-query-cachetime-vs-staletime-ec74defc483e 27 | staleTime: 10, 28 | retry: 3 29 | } 30 | } 31 | }) 32 | ) 33 | const [trpcClient] = useState(() => 34 | trpc.createClient({ 35 | transformer: superjson, 36 | links: [ 37 | httpBatchLink({ 38 | url: `${process.env['NEXT_PUBLIC_TRPC_API_URL']}trpc`, 39 | async headers() { 40 | return { 41 | /* In case you want to use Cognito */ 42 | // authorization: token.current 43 | } 44 | } 45 | }) 46 | ] 47 | }) 48 | ) 49 | 50 | return ( 51 | 52 | 53 | 54 | Purple Stack CHANGE_ME 55 | 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | export default App 63 | -------------------------------------------------------------------------------- /.github/workflows/release-prod.yaml: -------------------------------------------------------------------------------- 1 | name: Release Prod 2 | on: 3 | push: 4 | branches: 5 | - master 6 | permissions: 7 | contents: write 8 | jobs: 9 | create_release_notes: 10 | if: github.ref_type == 'branch' && github.ref_name == 'master' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v3 18 | with: 19 | version: 9 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: pnpm 26 | cache-dependency-path: pnpm-lock.yaml 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Release 32 | run: | 33 | pnpm semantic-release --dry-run | \ 34 | awk '/The next release version is ([0-9]+\.[0-9]+\.[0-9]+)/{ print $0 }' | \ 35 | awk '{split($0, array, "version is "); print array[2]}' > version.txt 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Check if there is anything worth a releasing 40 | run: | 41 | [ -s version.txt ] || exit 1 42 | 43 | - name: Create Release 44 | run: | 45 | gh api \ 46 | --method POST \ 47 | -H "Accept: application/vnd.github+json" \ 48 | -H "X-GitHub-Api-Version: 2022-11-28" \ 49 | /repos/${{github.repository}}/releases \ 50 | -f tag_name="v$(cat version.txt)" \ 51 | -f target_commitish=${{github.ref}} \ 52 | -f name="Production v$(cat version.txt)" \ 53 | -F generate_release_notes=true 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /services/frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /stacks/add-todo/AddTodoStateMachine.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Chain, 3 | INextable, 4 | IStateMachine, 5 | State 6 | } from 'aws-cdk-lib/aws-stepfunctions' 7 | import { 8 | DefinitionBody, 9 | Pass, 10 | Result, 11 | StateMachine 12 | } from 'aws-cdk-lib/aws-stepfunctions' 13 | import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks' 14 | import { Construct } from 'constructs' 15 | import type { App } from 'sst/constructs' 16 | import { Function } from 'sst/constructs' 17 | 18 | export interface StateMachineProps { 19 | servicePath: string 20 | } 21 | 22 | export class AddTodoStateMachine extends Construct { 23 | private props: StateMachineProps 24 | private app: App 25 | 26 | public stateMachine: IStateMachine 27 | 28 | constructor(scope: Construct, id: string, props: StateMachineProps) { 29 | super(scope, id) 30 | this.props = props 31 | this.app = scope.node.root as App 32 | 33 | // Your state machine definition is here 34 | const stateMachineDefinition: Chain = this.addTodoStep().next( 35 | this.examplePassStep() 36 | ) 37 | 38 | this.stateMachine = new StateMachine(this, 'StateMachine', { 39 | stateMachineName: `${this.app.stage}-${this.app.name}-add-todo`, 40 | definitionBody: DefinitionBody.fromChainable(stateMachineDefinition) 41 | }) 42 | } 43 | 44 | private addTodoStep(): State & INextable { 45 | return new LambdaInvoke(this, 'Get clients waiting for voucher', { 46 | lambdaFunction: new Function(this, 'AddTodoFunction', { 47 | handler: this.getHandlerPath('addTodo') 48 | }), 49 | payloadResponseOnly: true 50 | }) 51 | } 52 | 53 | private examplePassStep(): State & INextable { 54 | return new Pass(this, 'Just add example Pass', { 55 | result: Result.fromObject({ 56 | data: 'hello' 57 | }), 58 | resultPath: '$.passExampleResult' 59 | }) 60 | } 61 | 62 | private getHandlerPath(handlerName: string): string { 63 | return `${this.props.servicePath}/${handlerName}/index.handler` 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:react/recommended', 4 | 'plugin:@microsoft/eslint-plugin-sdl/node', 5 | 'plugin:@microsoft/eslint-plugin-sdl/common', 6 | 'plugin:@microsoft/eslint-plugin-sdl/react', 7 | 'plugin:security/recommended-legacy' 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 13, 11 | sourceType: 'module' 12 | }, 13 | settings: { 14 | react: { 15 | version: '18.2' 16 | } 17 | }, 18 | plugins: ['prettier', 'simple-import-sort', '@microsoft/eslint-plugin-sdl'], 19 | rules: { 20 | 'prettier/prettier': 'error', 21 | 'simple-import-sort/exports': 'error', 22 | 'simple-import-sort/imports': 'error' 23 | }, 24 | overrides: [ 25 | { 26 | files: ['*.ts', '*.tsx'], 27 | parser: '@typescript-eslint/parser', 28 | extends: ['plugin:@microsoft/eslint-plugin-sdl/typescript'], 29 | plugins: ['@typescript-eslint'], 30 | rules: { 31 | '@typescript-eslint/no-implied-eval': 'off', 32 | '@typescript-eslint/ban-ts-comment': [ 33 | 'error', 34 | { 35 | 'ts-expect-error': 'allow-with-description', 36 | 'ts-ignore': 'allow-with-description', 37 | 'ts-nocheck': true, 38 | 'ts-check': false 39 | } 40 | ], 41 | '@typescript-eslint/ban-types': ['off'], 42 | '@typescript-eslint/explicit-function-return-type': 'error', 43 | '@typescript-eslint/no-explicit-any': ['warn'], 44 | '@typescript-eslint/no-unused-vars': 'error', 45 | '@typescript-eslint/no-use-before-define': ['warn'], 46 | '@typescript-eslint/no-non-null-assertion': ['warn'], 47 | '@typescript-eslint/consistent-type-imports': 'error' 48 | } 49 | }, 50 | { 51 | files: ['*.tsx'], 52 | parser: '@typescript-eslint/parser', 53 | extends: ['plugin:@microsoft/eslint-plugin-sdl/typescript'], 54 | plugins: ['@typescript-eslint', 'react-hooks'], 55 | rules: { 56 | '@typescript-eslint/no-implied-eval': 'off', 57 | 'react-hooks/exhaustive-deps': 'warn', 58 | 'react-hooks/rules-of-hooks': 'error', 59 | 'react/prop-types': ['off'], 60 | 'react/react-in-jsx-scope': 'off' 61 | } 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purple-stack", 3 | "version": "3.0.0", 4 | "private": true, 5 | "type": "module", 6 | "engines": { 7 | "node": "20.x", 8 | "pnpm": ">=9.6 <10" 9 | }, 10 | "scripts": { 11 | "env": "env-cmd -f ./.env bash -c \"aws sts get-caller-identity --profile \\$AWS_PROFILE --no-cli-pager >/dev/null 2>&1 || aws sso login --profile \\$AWS_PROFILE\" && env-cmd -f ./.env", 12 | "prepare": "husky install", 13 | "lint": "eslint 'services' 'packages' 'constructs' --ext .ts,.tsx,.js,.jsx,.cjs,.mjs && prettier -c \"**/*.{json,html}\"", 14 | "lint:fix": "eslint 'services' 'packages' 'constructs' --ext .ts,.tsx,.js,.jsx,.cjs,.mjs --fix && prettier --write \"**/*.{json,html}\"", 15 | "dev": "pnpm run env -- sst dev", 16 | "build": "pnpm run env -- sst build", 17 | "deploy": "pnpm run env -- sst deploy", 18 | "remove": "pnpm run env -- sst remove", 19 | "sst-types": "pnpm run env -- sst types", 20 | "console": "pnpm run env -- sst console", 21 | "secrets": "pnpm run env -- sst secrets", 22 | "test": "vitest run", 23 | "test:watch": "vitest", 24 | "typecheck": "tsc --noEmit", 25 | "typecheck:all": "time pnpm run typecheck && time pnpm run -r --report-summary typecheck" 26 | }, 27 | "devDependencies": { 28 | "@commitlint/cli": "^18.6.1", 29 | "@commitlint/config-conventional": "^18.6.3", 30 | "@microsoft/eslint-plugin-sdl": "^0.2.2", 31 | "@purple/serverless-git-branch-stage-plugin": "^1.3.2", 32 | "@tsconfig/node20": "^20.1.4", 33 | "@types/node": "catalog:", 34 | "@typescript-eslint/eslint-plugin": "^6.21.0", 35 | "@typescript-eslint/parser": "^6.21.0", 36 | "@vitest/coverage-v8": "^1.5.0", 37 | "aws-cdk-lib": "2.124.0", 38 | "constructs": "10.3.0", 39 | "env-cmd": "^10.1.0", 40 | "eslint": "^8.57.0", 41 | "eslint-config-prettier": "^9.1.0", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-prettier": "^5.1.3", 44 | "eslint-plugin-react": "^7.34.1", 45 | "eslint-plugin-react-hooks": "^4.6.0", 46 | "eslint-plugin-security": "^2.1.1", 47 | "eslint-plugin-simple-import-sort": "^10.0.0", 48 | "husky": "^8.0.3", 49 | "lint-staged": "^15.2.2", 50 | "prettier": "^3.2.5", 51 | "prettier-plugin-tailwindcss": "^0.5.14", 52 | "semantic-release": "^23.0.8", 53 | "sst": "2.40.6", 54 | "typescript": "catalog:", 55 | "vite-tsconfig-paths": "^4.3.2", 56 | "vitest": "^1.6.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /stacks/frontend/index.ts: -------------------------------------------------------------------------------- 1 | import { Certificate } from 'aws-cdk-lib/aws-certificatemanager' 2 | import { ResponseHeadersPolicy } from 'aws-cdk-lib/aws-cloudfront' 3 | import type { ApiGatewayV1Api, StackContext } from 'sst/constructs' 4 | import { NextjsSite, use } from 'sst/constructs' 5 | 6 | // @ts-expect-error JS file 7 | import { getContentSecurityPolicy } from '../../services/frontend/next.config' 8 | import { TrpcApiStack } from '../trpc-api/stack' 9 | import { appDomain, appUrl, isProd } from '../utils' 10 | 11 | export function FrontendStack({ stack, app }: StackContext): void { 12 | const trpcApi = use(TrpcApiStack) 13 | 14 | const restApiDomain = (api: ApiGatewayV1Api): string => 15 | `${api.cdk.restApi.restApiId}.execute-api.${stack.region}.${stack.urlSuffix}` 16 | 17 | const responseHeadersPolicy = new ResponseHeadersPolicy( 18 | stack, 19 | 'ResponseHeaders', 20 | { 21 | /* In case you use report-uri 22 | Example: 23 | */ 24 | // customHeadersBehavior: { 25 | // customHeaders: [ 26 | // { 27 | // header: 'Report-To', 28 | // value: 29 | // '{"group":"default","max_age":31536000,"endpoints":[{"url":"https://myaxiory.report-uri.com/a/d/g"}],"include_subdomains":true}', 30 | // override: true 31 | // } 32 | // ] 33 | // }, 34 | securityHeadersBehavior: { 35 | contentSecurityPolicy: { 36 | /* In case you want to use Cognito */ 37 | // cognito-idp.${self.provider.region}.amazonaws.com 38 | 39 | // Add your own dependencies here. 40 | 41 | // You might wanna add your own report-uri 42 | // Example: "report-uri https://myaxiory.report-uri.com/r/d/csp/enforce;" 43 | contentSecurityPolicy: getContentSecurityPolicy( 44 | restApiDomain(trpcApi.restApi) 45 | ), 46 | override: true 47 | } 48 | } 49 | } 50 | ) 51 | 52 | new NextjsSite(stack, 'Next', { 53 | path: 'services/frontend', 54 | environment: { 55 | NEXT_PUBLIC_TRPC_API_URL: trpcApi.restApi.url 56 | /* In case you want to use Cognito */ 57 | // NEXT_PUBLIC_USER_POOL_ID: resources.userPool.userPoolId, 58 | // NEXT_PUBLIC_USER_POOL_CLIENT_ID: resources.userPool.userPoolId 59 | }, 60 | cdk: { 61 | responseHeadersPolicy: responseHeadersPolicy 62 | }, 63 | customDomain: { 64 | domainName: appDomain(app), 65 | hostedZone: isProd(app) 66 | ? 'purple-technology.io' // CHANGE_ME 67 | : 'staging.purple-technology.io', // CHANGE_ME 68 | cdk: { 69 | certificate: Certificate.fromCertificateArn( 70 | stack, 71 | 'certificate', 72 | isProd(app) ? 'CHANGE_ME' : 'CHANGE_ME' 73 | ) 74 | } 75 | } 76 | }) 77 | 78 | stack.addOutputs({ 79 | FrontendUrl: appUrl(app) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test Branch 2 | on: 3 | workflow_dispatch: 4 | push: 5 | 6 | permissions: 7 | id-token: write # This is required for requesting the JWT 8 | contents: read # This is required for actions/checkout 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # In order for commitlint to check historic commits 17 | 18 | # Cache cdk context to optimize OpenNext builds 19 | # https://docs.sst.dev/constructs/NextjsSite#nextjs-app-built-twice 20 | - name: CDK Context cache 21 | uses: actions/cache@v4 22 | with: 23 | path: cdk.context.json 24 | key: ${{ runner.os }}-${{ github.ref_name == 'master' && 'master' || 'staging' }}-${{ hashFiles('./stacks/**') }}-${{ hashFiles('./constructs/**') }}-${{ hashFiles('sst.config.ts') }}-${{ hashFiles('pnpm-lock.yaml') }} 25 | 26 | - name: Install pnpm 27 | uses: pnpm/action-setup@v3 28 | with: 29 | version: 9 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | cache: pnpm 36 | cache-dependency-path: pnpm-lock.yaml 37 | 38 | - name: Install Dependencies 39 | run: pnpm install 40 | 41 | - name: Commit Lint 42 | run: pnpm commitlint --from CHANGE_ME --to HEAD 43 | 44 | - name: Code Lint 45 | run: pnpm run lint 46 | 47 | - name: Configure AWS credentials 48 | uses: aws-actions/configure-aws-credentials@v4 49 | with: 50 | role-to-assume: ${{ github.ref == 'refs/heads/master' && 'CHANGE_ME' || 'CHANGE_ME' }} 51 | aws-region: eu-central-1 # Is not related to deployment region 52 | role-session-name: CHANGE_ME 53 | 54 | # https://nextjs.org/docs/pages/building-your-application/deploying/ci-build-caching#github-actions 55 | - name: Next.js cache 56 | uses: actions/cache@v4 57 | with: 58 | path: ${{ github.workspace }}/services/frontend/.next/cache 59 | # Generate a new cache whenever packages or source files change. 60 | key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('services/frontend/**/*.js', 'services/frontend/**/*.jsx', 'services/frontend/**/*.ts', 'services/frontend/**/*.tsx') }} 61 | # If source files changed but packages didn't, rebuild from a prior cache. 62 | restore-keys: | 63 | ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}- 64 | 65 | - name: Build SST 66 | run: pnpm sst build 67 | 68 | - name: Type check 69 | run: pnpm run typecheck:all && cat pnpm-exec-summary.json 70 | 71 | - name: Test 72 | run: pnpm run test 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purple Stack 2 | 3 |

4 | 9 Landscape Gradient@2x 5 |

6 | 7 | ![GitHub top language](https://img.shields.io/github/languages/top/purple-technology/purple-stack) 8 | ![GitHub last commit](https://img.shields.io/github/last-commit/purple-technology/purple-stack) 9 | ![GitHub](https://img.shields.io/github/license/purple-technology/purple-stack) 10 | 11 | ## What is Purple Stack 12 | 13 | Purple stack is a development stack designed by [Purple LAB](https://www.purple-technology.com/) for developing full-stack serverless apps. 14 | 15 | It's based on our 6+ years of experience with building big Serverless apps on AWS. 16 | 17 | You can read more about how Purple Stack was born on our [blog](https://blog.purple-technology.com/cs/pribeh-o-tom-ako-vznikol-purplestack/). 18 | 19 | ## Tech being used 20 | 21 | - Main language: [TypeScript](https://www.typescriptlang.org/) 22 | - Deployment framework: [SST.dev](https://sst.dev/) 23 | - Frontend: [React.js](https://react.dev) & [Next.js](https://nextjs.org/) 24 | - Code bundling: [ESbuild](https://esbuild.github.io/) 25 | - Package manager: [PNPM](https://pnpm.io/) 26 | - Linting: [ESlint](https://eslint.org/) & [Prettier](https://prettier.io/) 27 | - API protocol: [tRPC](https://trpc.io/) 28 | - Business workflows engine: [AWS Step Functions](https://aws.amazon.com/step-functions/) 29 | - CI/CD: [GitHub Actions](https://github.com/features/actions) 30 | - Static Application Security Testing (SAST) - [`eslint-plugin-security`](https://www.npmjs.com/package/eslint-plugin-security) & [@microsoft/eslint-plugin-sdl](https://www.npmjs.com/package/@microsoft/eslint-plugin-sdl) 31 | - Unit Tests - [Vitest](https://vitest.dev/) 32 | - Structured Logging: [AWS Lambda Powertools Logger](https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/) 33 | - Conventional Commits - [Commitlint](https://commitlint.js.org) 34 | - ... and more 35 | 36 | ## File structure 37 | 38 | ``` 39 | . 40 | ├── constructs 41 | │ └── # Here go sharable CDK constructs that 42 | │ # you can abstract for multiple services. 43 | │ # 44 | │ # Only individual TS files. No packages. 45 | ├── packages 46 | │ └── # Here goes any application code that 47 | │ # you need to share between services. 48 | │ # 49 | │ # Make sure the packages are created 50 | │ # as "npm" packages so that they have 51 | │ # package.json and tsconfig.ts files. 52 | ├── services 53 | │ └── # Here goes source code for indivudual 54 | │ # aws services. Inside these folders 55 | │ # are Lambda handlers and other relevant 56 | │ # source code. 57 | │ # 58 | │ # Make sure the services are created 59 | │ # as "npm" packages so that they have 60 | │ # package.json and tsconfig.ts files. 61 | └── stacks 62 | └── # Here goes AWS stacks definitions. 63 | # One folder for each service. 64 | # Make sure there is always file stack.ts 65 | # inside each folder. 66 | # 67 | # Only individual TS files. No packages. 68 | ``` 69 | 70 | ## ENV file 71 | 72 | Env file is quite simple in this case. Only `AWS_PROFILE` is necessary value. 73 | 74 | ### Example 75 | 76 | ```ini 77 | AWS_PROFILE=purple-technology 78 | ``` 79 | 80 | ## Setup 81 | 82 | There are some settings which need to be changed in order to make this boilerplate project work. 83 | 84 | ### GitHub Actions 85 | 86 | `.github/workflows/*` 87 | 88 | - replace all `CHANGE_ME` values 89 | - `role-session-name` - identifier of the application 90 | - `role-to-assume` - usually apps are deployed to "Production" and "Staging" AWS accounts. `master` branch gets deployed to "Production" and the rest goes to the "Staging" AWS account. Make sure to put there correct deployment roles. 91 | - [How to setup GitHub OpenID Connect on AWS](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) 92 | 93 | ### SST config 94 | 95 | `sst.config.ts` 96 | 97 | - Change app name. Current value: `name: 'purple-stack'` 98 | - Change regions. Current value: `region: stage === 'master' ? 'eu-central-1' : 'eu-central-1'` 99 | - Eventually enable tracing if you need. Current value: `tracing: 'disabled'` 100 | 101 | ## Best practices 102 | 103 | ### 1. Separate stateful resources to a separate stack 104 | 105 | You can use pre-defined `ResourceStack` for this. 106 | 107 | > #### Separate your application into multiple stacks as dictated by deployment requirements 108 | > There is no hard and fast rule to how many stacks your application needs. You'll usually end up basing the decision on your deployment patterns. Keep in mind the following guidelines: 109 | > 110 | > - It's typically more straightforward to keep as many resources in the same stack as possible, so keep them together unless you know you want them separated. 111 | > 112 | > - **Consider keeping stateful resources (like databases) in a separate stack from stateless resources. You can then turn on termination protection on the stateful stack. This way, you can freely destroy or create multiple copies of the stateless stack without risk of data loss.** 113 | > 114 | > - Stateful resources are more sensitive to construct renaming—renaming leads to resource replacement. Therefore, don't nest stateful resources inside constructs that are likely to be moved around or renamed (unless the state can be rebuilt if lost, like a cache). This is another good reason to put stateful resources in their own stack. 115 | 116 | Read more [here](https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html#best-practices-apps). 117 | 118 | ### 2. Don't turn off ESlint and TSconfig rules 119 | 120 | The rules are there for a reason. Always make sure to try all possible solutions to comply with the rules before disabling the rule. 121 | 122 | Every time you use `any` in the code a bunny dies. 123 | 124 |

125 | 126 |

127 | 128 | ### 3. Take advantage of all the features of SST 129 | 130 | SST has a lot of great quirks and features like `use`, `bind` etc. 131 | 132 | Read their [docs](https://docs.sst.dev/). 133 | -------------------------------------------------------------------------------- /services/frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {(trpcApiDomain: string) => string} */ 2 | export const getContentSecurityPolicy = (trpcApiDomain) => 3 | `base-uri 'self'; 4 | connect-src 'self' ${trpcApiDomain} *.google-analytics.com *.analytics.google.com analytics.google.com *.googletagmanager.com *.g.doubleclick.net *.google.com *.google.ad *.google.ae *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za *.google.co.zm *.google.co.zw *.google.cat; 5 | default-src 'self'; 6 | font-src 'self' data: fonts.gstatic.com fonts.googleapis.com; 7 | frame-src 'self' www.google.com td.doubleclick.net; 8 | img-src 'self' data: blob: ssl.gstatic.com www.gstatic.com *.google-analytics.com *.analytics.google.com *.googletagmanager.com *.g.doubleclick.net *.google.com *.google.ad *.google.ae *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za *.google.co.zm *.google.co.zw *.google.cat; 9 | object-src 'none'; 10 | script-src 'report-sample' 'self' 'unsafe-inline' 'unsafe-eval' www.google.com *.googletagmanager.com www.gstatic.com tagmanager.google.com; 11 | style-src 'report-sample' 'self' 'unsafe-inline' googletagmanager.com tagmanager.google.com fonts.googleapis.com; 12 | manifest-src 'self'; 13 | media-src 'self'; 14 | worker-src blob:; 15 | report-to default;`.replace(/\n/g, '') 16 | 17 | /** @type {import('next').NextConfig} */ 18 | const nextConfig = { 19 | reactStrictMode: true, 20 | eslint: { 21 | ignoreDuringBuilds: true 22 | } 23 | } 24 | 25 | // CSP via Next should be on localhost only 26 | // In the deployed app it's handled by CloudFront 27 | if (process.env.IS_LOCAL === 'true') { 28 | nextConfig.headers = async () => [ 29 | { 30 | source: '/(.*)', 31 | headers: [ 32 | { 33 | key: 'Content-Security-Policy', 34 | value: getContentSecurityPolicy( 35 | process.env.NEXT_PUBLIC_TRPC_API_URL.replace( 36 | 'https://', 37 | '' 38 | ).replace(/.com\/.*/, '.com') 39 | ) 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | 46 | export default nextConfig 47 | -------------------------------------------------------------------------------- /services/frontend/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import Image from 'next/image' 3 | 4 | import { Hello } from '@/components/Hello' 5 | import { trpc } from '@/helpers/trpc' 6 | 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | const Home: React.FC = () => { 10 | const todos = trpc.todos.list.useQuery({ id: '123' }) 11 | const createTodo = trpc.todos.create.useMutation() 12 | 13 | return ( 14 |
17 | 18 | 19 |
20 | API URL: 21 | {process.env['NEXT_PUBLIC_TRPC_API_URL']} 22 | 23 |
 24 | 					{typeof todos.data !== 'undefined'
 25 | 						? JSON.stringify(todos.data, null, 2)
 26 | 						: null}
 27 | 				
28 | 29 | 40 |
41 | 42 |
43 |

44 | Get started by editing  45 | src/pages/index.tsx 46 |

47 | 65 |
66 | 67 |
68 | Next.js Logo 76 |
77 | 78 | 147 |
148 | ) 149 | } 150 | 151 | export default Home 152 | --------------------------------------------------------------------------------