Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ node_modules
.vim

.env

.opencode

25 changes: 25 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# OpenCode Context for demo-notes-app

## Commands
- Build: `npm run build` (in packages/frontend)
- Dev: `npm run dev` (in packages/frontend)
- Lint: `npm run lint` (in packages/frontend)
- Test: `npm run test` (in packages/core)
- Single test: `sst shell vitest path/to/test.ts`

## Code Style
- TypeScript with strict typing
- React 19 with functional components and hooks
- Module exports pattern: `export module Name { export function method() {} }`
- Error handling: throw Error objects with descriptive messages
- Naming: camelCase for variables/functions, PascalCase for types/components
- Imports: group by external/internal, alphabetical order
- Formatting: 2-space indentation, trailing commas in objects
- Async/await for asynchronous operations
- Use object shorthand notation when variable names match keys

## Project Structure
- Monorepo with workspaces in packages/
- Core business logic in packages/core
- Frontend React app in packages/frontend
- API functions in packages/functions
45 changes: 26 additions & 19 deletions infra/api.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { table, secret } from "./storage";
import { auth } from "./auth";
import { stripeInfo } from "./billing";
import { users, notes, bucket } from "./storage";

// Create the API
export const api = new sst.aws.ApiGatewayV2("Api", {
transform: {
route: {
handler: {
link: [table, secret],
},
args: {
auth: { iam: true }
},
}
}
export const api = new sst.aws.Function("Api", {
url: true,
handler: "packages/functions/src/api/index.handler",
link: [auth, users, notes, bucket, stripeInfo],
});

api.route("GET /notes", "packages/functions/src/list.main");
api.route("POST /notes", "packages/functions/src/create.main");
api.route("GET /notes/{id}", "packages/functions/src/get.main");
api.route("PUT /notes/{id}", "packages/functions/src/update.main");
api.route("DELETE /notes/{id}", "packages/functions/src/delete.main");
api.route("POST /billing", "packages/functions/src/billing.main");
export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhook", {
url: $interpolate`${api.url}webhook`,
enabledEvents: ["customer.subscription.created"],
description: "Webhook for Stripe subscription created event",
});

api.addEnvironment({
STRIPE_WEBHOOK_SECRET: stripeWebhook.secret,
});

const anthropicKey = new sst.Secret("AnthropicKey");

export const opencontrol = new sst.aws.OpenControl("OpenControl", {
server: {
handler: "packages/opencontrol/src/server.handler",
policies: ["arn:aws:iam::aws:policy/ReadOnlyAccess"],
link: [notes, users, bucket, anthropicKey, stripeInfo],
},
});
49 changes: 10 additions & 39 deletions infra/auth.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
import { api } from "./api";
import { bucket } from "./storage";
import { users } from "./storage";
import { domain } from "./constants";

const region = aws.getRegionOutput().name;

export const userPool = new sst.aws.CognitoUserPool("UserPool", {
usernames: ["email"]
export const email = new sst.aws.Email("Email", {
sender: "[email protected]",
});

export const userPoolClient = userPool.addClient("UserPoolClient");

export const identityPool = new sst.aws.CognitoIdentityPool("IdentityPool", {
userPools: [
{
userPool: userPool.id,
client: userPoolClient.id,
export const auth = new sst.aws.Auth("Auth", {
issuer: {
link: [users, email],
handler: "packages/functions/src/auth.handler",
environment: {
FRONTEND_URL: $dev ? "http://localhost:5173" : `https://${domain}`,
},
],
permissions: {
authenticated: [
{
actions: ["s3:*"],
resources: [
$concat(bucket.arn, "/private/${cognito-identity.amazonaws.com:sub}/*"),
],
},
{
actions: [
"execute-api:*",
],
resources: [
$concat(
"arn:aws:execute-api:",
region,
":",
aws.getCallerIdentityOutput({}).accountId,
":",
api.nodes.api.id,
"/*/*/*"
),
],
},
],
},
});
24 changes: 24 additions & 0 deletions infra/billing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const stripeKey = new sst.Secret("StripeSecretKey");

export const stripeProduct = new stripe.Product("Notes", {
name: "Notes Pro Plan",
description: "A pro plan for the Notes app",
});

export const stripeSubscription = new stripe.Price("NotesSubscription", {
product: stripeProduct.id,
unitAmount: 1000,
currency: "usd",
recurring: {
interval: "month",
intervalCount: 1,
},
});

export const stripeInfo = new sst.Linkable("Stripe", {
properties: {
secretKey: stripeKey.value,
product: stripeProduct.id,
subscription: stripeSubscription.id,
}
});
1 change: 1 addition & 0 deletions infra/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const domain = $app.stage === "production" ? "demo.sst.dev" : undefined;
19 changes: 14 additions & 5 deletions infra/storage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
// Create an S3 bucket
export const bucket = new sst.aws.Bucket("Uploads");

// Create the DynamoDB table
export const table = new sst.aws.Dynamo("Notes", {
// Create the DynamoDB users table
export const users = new sst.aws.Dynamo("Users", {
fields: {
userId: "string",
email: "string",
},
primaryIndex: { hashKey: "userId" },
globalIndexes: {
emailIndex: { hashKey: "email" },
},
});

// Create the DynamoDB notes table
export const notes = new sst.aws.Dynamo("Notes", {
fields: {
userId: "string",
noteId: "string",
},
primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
});

// Create a secret for Stripe
export const secret = new sst.Secret("StripeSecretKey");
12 changes: 4 additions & 8 deletions infra/web.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { api } from "./api";
import { auth } from "./auth";
import { bucket } from "./storage";
import { userPool, identityPool, userPoolClient } from "./auth";

const region = aws.getRegionOutput().name;
import { domain } from "./constants";

export const frontend = new sst.aws.StaticSite("Frontend", {
domain,
path: "packages/frontend",
build: {
output: "dist",
command: "npm run build",
},
domain: $app.stage === "production" ? "demo.sst.dev" : undefined,
environment: {
VITE_REGION: region,
VITE_API_URL: api.url,
VITE_AUTH_URL: auth.url,
VITE_BUCKET: bucket.name,
VITE_USER_POOL_ID: userPool.id,
VITE_IDENTITY_POOL_ID: identityPool.id,
VITE_USER_POOL_CLIENT_ID: userPoolClient.id,
},
});
Loading