ledgerflow-backend/README.md
Adam Moussa 59127d5ab8 Initial commit — LedgerFlow backend
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>
2026-04-02 18:19:38 -04:00

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

  1. Go to Google Cloud Console
  2. Create a project (or use existing)
  3. Enable the "Google Identity" API
  4. Create an OAuth 2.0 Client ID → Web Application
  5. Add your frontend domain to Authorized JavaScript origins
    • http://localhost:3000 (dev)
    • https://your-production-domain.com (prod)
  6. No redirect URIs needed — using Google Identity Services (new flow)
  7. Copy the Client ID to GOOGLE_CLIENT_ID

4. Set up AWS B2B EDI (optional — app works without it)

  1. Go to AWS B2B Data Interchange
  2. Create a Profile (your company details)
  3. Create a Partnership with your trading partner
  4. Create a Capability → choose X12 / 810 Invoice
  5. Create a Transformer for inbound/outbound mapping
  6. 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:

  1. Create lambdas/myroute/index.js exporting handler
  2. Add to infra/lib/ledgerflow-stack.js:
    const myFn = new lambda.Function(this, "MyFn", { ...lambdaDefaults, code: lambda.Code.fromAsset("../lambdas/myroute"), ... });
    addRoutes("/myroute", myFn);
    
  3. 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-orders DynamoDB table via ship_to field (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.