Lambda-based serverless backend with Google SSO, purchase orders, invoices, and X12 810 EDI generation for Amazon Payee Central. Includes bill-to/ship-to address support from Coupa purchase-orders table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.5 KiB
LedgerFlow Backend
Node.js serverless backend for the LedgerFlow B2B accounting app. Runs entirely on AWS Lambda + API Gateway with Google SSO authentication.
Architecture
Browser (Google Sign-In)
│
│ Bearer: Google ID Token
▼
API Gateway (HTTP API)
│
├── Lambda Authorizer ──── verifies Google JWT ──── DynamoDB (sessions)
│
├── POST /auth/me ← public, verifies token, returns user
├── GET /auth/config ← public, returns Google Client ID
│
├── /pos/** ← Purchase Orders Lambda
│ ├── GET /pos list (paginated)
│ ├── POST /pos create
│ ├── GET /pos/:id get one
│ ├── PUT /pos/:id update
│ ├── DELETE /pos/:id delete
│ ├── POST /pos/import bulk import
│ └── GET /pos/dynamo-scan proxy scan of customer's DynamoDB table
│
├── /invoices/** ← Invoices Lambda
│ ├── GET /invoices list
│ ├── POST /invoices create (bills against PO)
│ ├── GET /invoices/:id get one
│ ├── PUT /invoices/:id update
│ ├── PATCH /invoices/:id/status status transition
│ └── DELETE /invoices/:id delete (drafts only)
│
├── /settings/** ← Settings Lambda
│ ├── GET /settings get user settings
│ └── PUT /settings update settings
│
└── /edi/** ← EDI Lambda
├── POST /edi/submit submit to AWS B2B Data Interchange
├── POST /edi/preview preview X12 document (no submission)
├── GET /edi/transactions transaction history
├── GET /edi/transactions/:id one transaction
└── GET /edi/status/:isaControl poll for 997 ACK
DynamoDB Tables:
ledgerflow-pos Purchase Orders (app-managed)
ledgerflow-invoices Invoices
ledgerflow-edi-transactions EDI transaction log
ledgerflow-sessions Google auth sessions (TTL 7 days)
ledgerflow-settings User/company settings
External Tables:
purchase-orders Coupa POs (read-only, ship_to address for EDI)
S3 Buckets (EDI file exchange):
ledgerflow-edi-input-{account}
ledgerflow-edi-output-{account}
Prerequisites
- Node.js 20+
- AWS CLI configured (
aws configure) - AWS CDK bootstrapped (
npx cdk bootstrap) - Google Cloud project with OAuth 2.0 credentials
- AWS B2B Data Interchange configured (optional for MVP)
Setup
1. Install dependencies
npm install
cd infra && npm install
2. Configure environment
cp .env.example .env
# Edit .env with your values
Key variables:
| Variable | Where to get it |
|---|---|
GOOGLE_CLIENT_ID |
Google Cloud Console → OAuth 2.0 Client ID |
ALLOWED_DOMAINS |
Your company's Google Workspace domain, e.g. acme.com |
ALLOWED_ORIGIN |
Your frontend URL, e.g. https://app.acme.com |
EDI_PARTNERSHIP_ID |
AWS B2B Data Interchange → Partnerships |
EDI_TRANSFORMER_ID |
AWS B2B Data Interchange → Transformers (X12 810) |
EDI_SENDER_ID |
Your ISA Sender ID (padded to 15 chars) |
EDI_RECEIVER_ID |
Your trading partner's ISA ID |
3. Set up Google OAuth
- Go to Google Cloud Console
- Create a project (or use existing)
- Enable the "Google Identity" API
- Create an OAuth 2.0 Client ID → Web Application
- Add your frontend domain to Authorized JavaScript origins
http://localhost:3000(dev)https://your-production-domain.com(prod)
- No redirect URIs needed — using Google Identity Services (new flow)
- Copy the Client ID to
GOOGLE_CLIENT_ID
4. Set up AWS B2B EDI (optional — app works without it)
- Go to AWS B2B Data Interchange
- Create a Profile (your company details)
- Create a Partnership with your trading partner
- Create a Capability → choose X12 / 810 Invoice
- Create a Transformer for inbound/outbound mapping
- Copy the Partnership ID and Transformer ID to your
.env
5. Deploy
# Load env vars and deploy
export $(cat .env | xargs)
npm run deploy
CDK will output your API Gateway URL:
Outputs:
LedgerFlow.ApiUrl = https://abc123.execute-api.us-east-1.amazonaws.com
6. Connect the frontend
In your accounting-app.html, add before </body>:
<script>
window.LEDGERFLOW_API_URL = "https://abc123.execute-api.us-east-1.amazonaws.com";
window.GOOGLE_CLIENT_ID = "your-client-id.apps.googleusercontent.com";
</script>
Or set these via your hosting environment (Cloudflare Pages, S3+CloudFront, etc.).
Replace localStorage calls in accounting-app.html with window.LedgerFlowAPI.* calls
(the API client is defined in frontend-updates/login.html).
Local Development
For local testing, use AWS SAM or run Lambdas directly:
# Test a Lambda locally
node -e "
const fn = require('./lambdas/pos');
fn.handler({
requestContext: { http: { method: 'GET' } },
rawPath: '/pos',
queryStringParameters: {}
}, {}, { email: 'dev@test.com' }).then(console.log);
"
Or use AWS SAM CLI:
sam local start-api --template infra/template.yaml
Security Notes
- Google tokens are verified on every request by the Lambda Authorizer — they cannot be forged
- AWS credentials never leave Lambda environment variables — the frontend only holds a Google ID token
- DynamoDB access is scoped per Lambda via least-privilege IAM roles (CDK handles this)
- ALLOWED_DOMAINS restricts login to your company's Google Workspace — set this in production
- ALLOWED_ORIGIN restricts CORS to your frontend domain — set this before going to production
- Session TTL: DynamoDB sessions auto-expire after 7 days of inactivity
Extending
Add a new Lambda route:
- Create
lambdas/myroute/index.jsexportinghandler - Add to
infra/lib/ledgerflow-stack.js:const myFn = new lambda.Function(this, "MyFn", { ...lambdaDefaults, code: lambda.Code.fromAsset("../lambdas/myroute"), ... }); addRoutes("/myroute", myFn); - Re-deploy:
npm run deploy
Add a new DynamoDB table:
const myTable = new dynamo.Table(this, "MyTable", {
tableName: "ledgerflow-mytable",
partitionKey: { name: "id", type: dynamo.AttributeType.STRING },
billingMode: dynamo.BillingMode.PAY_PER_REQUEST,
});
myTable.grantReadWriteData(myFn);
File Structure
ledgerflow-backend/
├── lambdas/
│ ├── authorizer/ Google JWT Lambda Authorizer
│ │ └── index.js
│ ├── auth/ Auth endpoints (/auth/me, /auth/config)
│ │ └── index.js
│ ├── pos/ Purchase Orders CRUD + DynamoDB import
│ │ └── index.js
│ ├── invoices/ Invoices CRUD + PO balance tracking
│ │ └── index.js
│ └── edi/ AWS B2B EDI submission + transaction log
│ └── index.js
├── shared/
│ ├── index.js Shared utilities (responses, DynamoDB client, helpers)
│ └── package.json
├── infra/
│ ├── bin/app.js CDK entry point
│ ├── lib/ CDK stack (all AWS resources)
│ ├── cdk.json
│ └── package.json
├── .env.example
├── .gitignore
└── package.json
EDI (X12 810) Details
The EDI lambda generates X12 004010 810 Invoice documents for Amazon Payee Central.
Address handling:
- Bill-To (N1*BT): Hardcoded to Amazon.com Services LLC, 410 Terry Ave N, Seattle WA 98109-5210
- Ship-To (N1*ST): Pulled from the external
purchase-ordersDynamoDB table viaship_tofield (Coupa PO data) - Remit-To / Payee (N1*RI, N1*PE): From company settings
Submit payload:
{
"invoiceId": "INV-0001-id",
"txType": "810",
"billTo": {
"name": "Override Name",
"address": "123 Main St, City, ST 12345, US"
}
}
billTo is optional — defaults to Amazon HQ if omitted.