OAUTH PREVIEW — Create and manage OAuth clients in the Developer PortalStable portal →

OAuth sample applications

Use a sample application to see Lunch Money OAuth working end to end before you adapt the flow to your own architecture. Choose the confidential sample when a trusted server will hold credentials, or the native sample when an installed iOS or Android application will hold them. The two architectures have different security boundaries and are documented separately.

Confidential Node.js and TypeScript sample

This walkthrough is for confidential applications
The walkthrough and teaching-code guide below apply to the confidential Node.js sample, where a trusted server holds the client secret and credentials. For an installed iOS or Android application that holds its own credentials without a client secret, skip to the Native Expo sample.

lm-oauth-confidential-node-example is a teaching sample for a confidential, server-side web client. It uses Node.js, TypeScript, and the maintained openid-client library to demonstrate:

Teaching sample, not a production application
The sample uses a fixed demonstration user and in-memory stores. It does not provide real application authentication, durable multi-user isolation, production credential storage, deployment configuration, or operational controls. Review its security guidance and production checklist before adapting any part of it.

Try OAuth with your Lunch Money account

Use this walkthrough to authorize a real Lunch Money OAuth client, call the Lunch Money API, optionally refresh its credentials, revoke access, and repeat the flow. The sample is a confidential, server-side web client built with Node.js and TypeScript.

openid-client is the recommended and supported third-party Node.js library for working with Lunch Money's OAuth interfaces. The sample uses it for discovery, authorization, callback validation, token exchange, refresh, and revocation while keeping every credential on the server.

This is a real OAuth flow. You will create a real client, authorize access to a real Lunch Money budgeting account, receive real credentials, and call the real Lunch Money API. Only the automated tests use mocked Lunch Money responses.

Before you begin

You need:

While a client is in development, only the Lunch Money user who created it can authorize it. Use that same Lunch Money user when the sample sends you through authorization. Other Lunch Money users can authorize the client only after it has been reviewed and approved.

Three actions and identities are involved:

Your real Lunch Money account
  creates and owns the OAuth client
  authorizes access to a real budgeting account
                    ↓
          Sample OAuth callback
                    ↓
Credentials stored under local-demo-user
(the stand-in for your application's authenticated user)

The sample uses the client you register in the steps below; it does not create or modify that client. Client management remains in the Developer Portal. The automated tests use mocked HTTP responses, but the running sample connects to the configured real Lunch Money authorization and API services.

1. Download and install the sample

git clone https://github.com/lunch-money/lm-oauth-confidential-node-example.git
cd lm-oauth-confidential-node-example
npm ci
npm run build

2. Register a confidential client

In the Lunch Money Developer Portal, create a Confidential web client with:

Lunch Money uses the complete scope set registered for the client. The sample therefore does not send a scope parameter during authorization.

Registered scopes cannot be changed. Decide whether you want to test refresh before creating the client: adding offline_access later requires a replacement client registered with both me:read and offline_access, followed by a new authorization.

You do not need offline_access to authorize the client, call /v2/me, revoke access, or authorize again.

3. Configure the local process

In the same terminal, set the client ID and secret from the Developer Portal:

export OAUTH_CLIENT_ID='YOUR_CLIENT_ID'
export OAUTH_CLIENT_SECRET='YOUR_CLIENT_SECRET'
export OAUTH_REDIRECT_URI='http://localhost:4002/oauth/callback'
export LUNCH_MONEY_API_BASE_URL='https://api-alpha.lunchmoney.app/v2'

The Developer Portal replaces https://api-alpha.lunchmoney.app/v2 with the API base URL for the environment where you registered the client, so the alpha guide configures alpha explicitly. If you are reading this walkthrough in the sample repository with a production client, omit the LUNCH_MONEY_API_BASE_URL export; the sample defaults to https://api.lunchmoney.dev. For a beta or alpha client, use the exact URL supplied by Lunch Money. The client registration and API base URL must belong to the same environment. The sample uses that value for OAuth discovery, token operations, and Lunch Money API requests. A real application should also use OAuth discovery instead of hard-coding authorization, token, and revocation endpoint URLs. PORT is optional and defaults to 4002.

SESSION_SECRET is also optional for this local sample. If it is absent, the process creates a new random cookie-signing secret when it starts. Because all browser sessions, authorization attempts, and credentials are held in memory and cleared on restart, the generated secret can be cleared at the same time. A production application must instead provide a strong SESSION_SECRET, keep it stable across restarts, and not rotate it for each OAuth authorization.

Alternatively, copy env.example to .env as described in Load settings from a .env file.

Keep credentials private
Keep the client secret in a private local environment or secret manager. Never paste it into documentation, AI chats or prompts, committed files, screenshots, browser code, logs, support requests, or commands retained in shared shell history.

4. Start the sample

npm run dev

Open http://localhost:4002 in your browser.

If a form reports Invalid CSRF token, reload the page and try again. This commonly happens when the development server restarts and clears its in-memory browser session while an older page remains open. Simply leaving the unchanged running sample open does not expire the form token.

5. Authorize and call /v2/me

  1. Choose Connect Lunch Money.
  2. Sign into Lunch Money as the user who created the development client.
  3. Select one of that user's budgeting accounts and approve access.
  4. After the browser returns to http://localhost:4002/oauth/callback, expand How this authorization was processed to follow the callback, server-side code exchange, /v2/me identity check, and resulting active-user decision. Then choose Call /v2/me.

The sample calls GET /v2/me from its Node.js server using the server-held access token. It validates the response against the documented userObject schema and displays the returned profile fields. The access token is never sent to the browser.

The processing panel links each teaching step to its implementation. It deliberately does not reveal credential values. To see the normalized credential response while developing locally, set a debugger breakpoint immediately after exchangeCallback() returns in completeAuthorization, then inspect tokenSet. Do not print tokens in browser output or logs.

If you want to verify the access token outside the sample, copy it from the local debugger and read it silently into a temporary shell variable. This keeps the credential itself out of shell history:

read -s LM_ACCESS_TOKEN
curl --fail-with-body \
  --header "Authorization: Bearer ${LM_ACCESS_TOKEN}" \
  "${LUNCH_MONEY_API_BASE_URL}/v2/me"
unset LM_ACCESS_TOKEN

Press Enter after pasting the token at the hidden prompt. Treat it as a live secret: use a private local terminal, do not paste it directly into a command, and clear the variable when finished. This debugger workflow is for understanding and testing the sample. A production application should inspect encrypted credential records only through privileged operational tooling that redacts secret values.

Open Lunch Money's Connected Apps page in another tab, keeping the sample available so you can return to it. The client you just authorized should appear with the details you registered. If you plan to exercise refresh, return to the sample without revoking access yet; revocation ends this authorization's continuing access.

6. Optionally refresh access

If you registered the client with offline_access, choose Refresh access token, then choose Call /v2/me again.

Lunch Money replaces both the access token and refresh token after a successful refresh. The sample stores the complete replacement credential set on the server before reporting success. Calling /v2/me again confirms that it is using the replacement access token.

If you registered only me:read, Lunch Money does not issue a refresh token and the refresh button does not appear. Skip this step.

The sample allows an immediate refresh so you can observe the flow. A production application normally decides when to refresh using expiration information from the token response or after an API authentication failure.

7. Disconnect the active budget

Choose Disconnect active budget.

The sample then calls /v2/me with the old access token, requires Lunch Money to return 401, and deletes its locally stored credential. This verification demonstrates that the old access token no longer works.

To repeat the flow, choose Connect Lunch Money and authorize the client again.

8. Try user-initiated revocation

To see what happens when a Lunch Money user disconnects the client:

  1. Authorize the sample again.
  2. Open Connected Apps. Keep the sample available in another tab so you can return to it after revoking the authorization.
  3. Revoke the client's access there.
  4. Return to the sample and choose Call /v2/me.

The request should fail because Lunch Money no longer accepts the stored access token. Choose Forget local credential only to remove the now-unusable local credential before starting again.

Local reset is not revocation

Forget local credential only clears the active budget's locally stored credential after confirmation. It does not contact Lunch Money and does not revoke active access.

When access is still active, revoke it first through Disconnect active budget or Lunch Money's Connected Apps page. Forget the local credential by itself only when the remote authorization has already been revoked or you deliberately want to clear this disposable local demonstration.

Why localhost works

Lunch Money redirects your browser to the registered callback, and your browser connects to the sample running on your computer. Lunch Money's server does not initiate a connection to localhost, so this walkthrough does not require a public deployment or tunnel.

Keep the sample local

The sample uses a fixed application identity and keeps browser sessions, authorization attempts, credentials, and refresh coordination in memory. Run it locally and do not expose it as a public application. Use a test budgeting account when practical. Restarting the sample clears its in-memory state.

After the flow succeeds, connect each action you performed to the code that implemented it, then decide how those responsibilities fit into your own application.

Before adapting the code, read the repository's OAuth code guide, security model, production checklist, and troubleshooting guide.

Understand the confidential teaching code

The open-source sample you just ran is designed to make the Lunch Money integration code easy to find, understand, and adapt. The modules under src/oauth show the OAuth responsibilities that a confidential server-side application needs: discovery, authorization attempts, callback validation, credential ownership and storage, API access, optional refresh, revocation, and safe error handling.

The sample represents an authenticated application user with a fixed local demo identity. After authorization, it uses the Lunch Money user id returned by GET /v2/me to choose which saved budgets are visible; credentials retained for another Lunch Money user remain hidden. A production confidential application replaces the fixed demo identity with the application user from its authenticated server-side session and identifies each owned connection by the returned Lunch Money user id and budget-account account_id pair.

Treat these modules as a working reference, not as a package to copy unchanged into production. The in-memory implementation demonstrates serialized refresh and complete credential replacement for one applicationUserId and connectionId, but it does not provide durable encrypted storage, database transactions, distributed per-connection locking, failed-persistence recovery across restarts, multi-instance coordination, or tenant isolation. The sample keeps those application-specific decisions outside the teaching code so you can see exactly where your implementation must provide them.

When refresh succeeds, the sample stores the replacement access token, refresh token, scope, and expiration metadata together without displaying token values. Your refresh-capable application must preserve that boundary, serialize refreshes for each application user and connection, and atomically store the complete rotated credential set. Retry transient failures according to your application's policy, but treat invalid_grant as terminal: stop refreshing, discard unusable credentials, and require reauthorization. Disconnect must invalidate the refresh token as well as the current access token so continuing access cannot resume.

Follow the flow through the source

Start with the src/oauth guide. It provides the maintained reading order for the framework-independent OAuth modules and connects each file to the stage of the flow it implements.

The OAuth teaching code is isolated under src/oauth. Hono, sessions, cookies, pages, and other runnable presentation code live under src/scaffolding, so you can replace the framework without obscuring the protocol flow. The sample's architecture guide explains this boundary, and its OAuth tests mirror the recommended reading order, including focused refresh tests. If your result differs, compare the stage that failed with the sample's troubleshooting guide and the public OAuth troubleshooting guide.

Native Expo sample

Choose lm-oauth-native-expo-example for an iOS or Android application installed on the user's device when there is no trusted backend that can hold a client secret. It uses Expo, the platform authentication browser, authorization code with S256 PKCE, and platform secure storage as a native/public client. Its optional offline_access path demonstrates rotated refresh credentials and terminal reauthorization without introducing a backend or client secret.

Do not choose it merely because your interface is responsive. Choose it because the installed application performs OAuth and owns its credential. If a trusted server performs the code exchange and stores credentials, use the confidential Node.js sample above instead.

Unlike the confidential sample, the native sample has no separate application account or server-side application-user boundary. It identifies local connections directly by the Lunch Money user id and budget-account account_id returned by GET /v2/me. It can retain connections for more than one Lunch Money user on the device, but only the active user's budgets are visible or selectable. Applications that add their own sign-in system must put that pair inside the authenticated application user's storage and ownership boundary.

The native sample defaults to the production API at https://api.lunchmoney.dev. This Developer Portal is configured for https://api-alpha.lunchmoney.app/v2. If this differs from production, set the sample's API-base override before starting it:

export EXPO_PUBLIC_LUNCH_MONEY_API_BASE_URL='https://api-alpha.lunchmoney.app/v2'

Register the OAuth client in the same environment as that API base URL. A client registration from one environment cannot be used with another environment's authorization server and API.

The native repository's walkthrough is the canonical hands-on guide; complete the registration, development-build, authorization, /v2/me, optional refresh, revocation, and local-reset walkthrough there.

Follow the native flow through the source

Start with the native sample's src/oauth guide. It provides the maintained reading order from public configuration and Expo's system-browser adapter through callback validation, secure credential storage, optional refresh, revocation, and the workflow connected to the sample screen.

The OAuth teaching code is isolated under src/oauth; React Native UI and platform storage adapters live under src/scaffolding. Use the sample's OAuth tests to follow the expected success and failure behavior, then review its security guidance and production checklist before adapting it.

Next: Develop and test your OAuth application and operate it securely.