Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ ynab_connect
# docs
docs/.vitepress/dist
docs/.vitepress/cache
docs/.vitepress/config-schema.json

# Claude Code
.claude
16 changes: 9 additions & 7 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,20 @@ export default defineConfig({
sidebar: [
{
text: "Get Started",
items: [{ text: "Quick Start", link: "/quick-start" }],
},
{
text: "Configuration",
items: [
{ text: "Quick Start", link: "/quick-start" },
{ text: "Configuration", link: "/configuration" },
{ text: "Overview", link: "/configuration" },
{ text: "Configuration Reference", link: "/config-reference" },
{ text: "Browser Setup", link: "/browser" },
{ text: "SMS Forwarding for 2FA", link: "/guide/sms-forwarding" },
],
},
{
text: "Guides",
items: generateSidebarItems("guide", "guide"),
},
{
text: "Features",
items: [{ text: "Browser", link: "/browser" }],
items: [{ text: "Create YNAB Token", link: "/guide/create-ynab-token" }],
},
{
text: "Connectors",
Expand Down
51 changes: 44 additions & 7 deletions docs/browser.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
---
title: Browser
title: Browser Setup
---
# Set up a browser
Some sources require a browser to be configured. `ynab-connect` uses Puppeteer for browser automation.
# Browser Setup

Some connectors require browser automation to access your data. `ynab-connect` uses Puppeteer for browser automation.

## When is this needed?

Some financial institutions do not provide an API to access your data. In these cases, `ynab-connect` can use a headless browser to log in to your account and retrieve your balance.

Connectors that require browser automation will indicate this in their documentation.

## Configuration

Set up a headless browser service like [Browserless](https://github.com/browserless/browserless) and add the following to your configuration:

```yaml
browser:
endpoint: "wss://your-browserless-endpoint"
```

### Using Browserless Cloud

The easiest way to get started is with [Browserless Cloud](https://www.browserless.io/):

1. Sign up for a free account
2. Get your connection URL (includes your token)
3. Add it to your configuration:

```yaml
browser:
endpoint: "wss://chrome.browserless.io?token=YOUR_TOKEN"
```

### Self-Hosting Browserless

You can also run Browserless yourself using Docker:

```bash
docker run -d -p 3000:3000 browserless/chrome
```

Then configure:

Set up a headless browser like [Browserless](https://github.com/browserless/browserless) and add the following to your configuration:
```yaml
browser:
endpoint: "ws://your-browserless-endpoint:3000"
endpoint: "ws://localhost:3000"
```

## Why might a browser be needed?
Some sources do not provide an API to access your data. In these cases, `ynab-connect` can use a headless browser to log in to your account and retrieve your data.
See the [Browserless documentation](https://docs.browserless.io/) for more details on self-hosting.
41 changes: 41 additions & 0 deletions docs/config-reference.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import fs from "node:fs";
import path from "node:path";

interface JsonSchemaProperty {
type?: string;
properties?: Record<string, JsonSchemaProperty>;
items?: JsonSchemaProperty;
anyOf?: JsonSchemaProperty[];
required?: string[];
minLength?: number;
maxLength?: number;
minimum?: number;
maximum?: number;
exclusiveMinimum?: number;
exclusiveMaximum?: number;
pattern?: string;
format?: string;
default?: unknown;
const?: unknown;
minItems?: number;
maxItems?: number;
additionalProperties?: boolean;
description?: string;
}

interface JsonSchema {
$schema?: string;
type?: string;
properties?: Record<string, JsonSchemaProperty>;
required?: string[];
additionalProperties?: boolean;
description?: string;
}

export default {
load() {
const schemaPath = path.join(__dirname, ".vitepress/config-schema.json");
const schema: JsonSchema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
return { schema };
},
};
230 changes: 230 additions & 0 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
---
title: Configuration Reference
---

<script setup>
import { data } from './config-reference.data'

const schema = data.schema

// Helper to format property type
const formatType = (prop) => {
if (prop.type === 'string' && prop.format === 'email') return 'Email'
if (prop.type === 'string' && prop.const) return `"${prop.const}"`
if (prop.type === 'integer') return 'Integer'
if (prop.type === 'string') return 'String'
if (prop.type === 'object') return 'Object'
if (prop.type === 'array') return 'Array'
return prop.type || 'Unknown'
}

// Check if a property is required
const isRequired = (parentRequired, key) => {
return parentRequired?.includes(key) ? 'Yes' : 'No'
}

// Get account types from schema
const getAccountTypes = () => {
const accountsProperty = schema.properties?.accounts
if (!accountsProperty?.items?.anyOf) return []

return accountsProperty.items.anyOf.map(typeSchema => {
const typeValue = typeSchema.properties?.type?.const
return {
type: typeValue,
schema: typeSchema
}
})
}

const accountTypes = getAccountTypes()

// Helper to format connector type name (e.g., "uk_student_loan" -> "Uk Student Loan")
const formatConnectorName = (type) => {
return type.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
}

// Helper to format connector slug (e.g., "uk_student_loan" -> "uk-student-loan")
const formatConnectorSlug = (type) => {
return type.replace(/_/g, '-')
}

// Get top-level properties (excluding 'accounts' which we handle separately)
const getTopLevelSections = () => {
const props = schema.properties || {}
return Object.entries(props)
.filter(([key]) => key !== 'accounts')
.map(([key, prop]) => ({ key, prop }))
}

const topLevelSections = getTopLevelSections()

// Get common account fields (fields that appear in all account types)
const getCommonAccountFields = () => {
if (accountTypes.length === 0) return []

const firstType = accountTypes[0].schema.properties
const commonFields = []

for (const [key, prop] of Object.entries(firstType)) {
// Skip 'type' field as it has different const values for each account type
if (key === 'type') continue

// Check if this field exists in all account types
const isCommon = accountTypes.every(at => at.schema.properties[key])
if (isCommon) {
commonFields.push({ key, prop })
}
}

return commonFields
}

const commonAccountFields = getCommonAccountFields()

// Get account-specific fields (excluding common fields)
const getAccountSpecificFields = (accountType) => {
return Object.entries(accountType.schema.properties)
.filter(([key]) => !commonAccountFields.some(f => f.key === key))
.map(([key, prop]) => ({ key, prop }))
}
</script>

# Configuration Reference

{{ schema.description }}

## Top-Level Configuration

The configuration file is a YAML file with the following top-level properties:

<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="section in topLevelSections" :key="section.key">
<td><code>{{ section.key }}</code></td>
<td>{{ formatType(section.prop) }}</td>
<td>{{ isRequired(schema.required, section.key) }}</td>
<td>{{ section.prop.description || '' }}</td>
</tr>
<tr>
<td><code>accounts</code></td>
<td>Array</td>
<td>{{ isRequired(schema.required, 'accounts') }}</td>
<td>{{ schema.properties?.accounts?.description || '' }}</td>
</tr>
</tbody>
</table>

<div v-for="section in topLevelSections" :key="section.key">

## {{ section.key.charAt(0).toUpperCase() + section.key.slice(1) }} Configuration

{{ section.prop.description }}

<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Required</th>
<th v-if="section.prop.default">Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="(prop, key) in section.prop.properties" :key="key">
<td><code>{{ key }}</code></td>
<td>{{ formatType(prop) }}</td>
<td>{{ isRequired(section.prop.required, key) }}</td>
<td v-if="section.prop.default">{{ prop.default !== undefined ? prop.default : '-' }}</td>
<td>{{ prop.description || '' }}</td>
</tr>
</tbody>
</table>

<div v-if="section.key === 'ynab'">

See the [Create YNAB Token](/guide/create-ynab-token) guide for instructions on obtaining these values.

</div>

<div v-if="section.key === 'browser'">

See the [Browser](/browser) documentation for more information.

</div>

</div>

## Accounts Configuration

{{ schema.properties?.accounts?.description }}

### Common Account Fields

All account types share these fields:

<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="field in commonAccountFields" :key="field.key">
<td><code>{{ field.key }}</code></td>
<td>{{ formatType(field.prop) }}</td>
<td>Yes</td>
<td>{{ field.prop.description || '' }}</td>
</tr>
</tbody>
</table>

### Account Types

<div v-for="accountType in accountTypes" :key="accountType.type">

#### {{ formatConnectorName(accountType.type) }}

**Type:** `{{ accountType.type }}`

<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="field in getAccountSpecificFields(accountType)" :key="field.key">
<td><code>{{ field.key }}</code></td>
<td>{{ formatType(field.prop) }}</td>
<td>{{ isRequired(accountType.schema.required, field.key) }}</td>
<td>{{ field.prop.description || '' }}</td>
</tr>
</tbody>
</table>

<p>See the <a :href="`/connectors/${formatConnectorSlug(accountType.type)}`">{{ formatConnectorName(accountType.type) }}</a> connector documentation for setup instructions.</p>

</div>

## Notes

- You can configure multiple accounts of the same type
- The `interval` field uses cron syntax. Use [crontab.guru](https://crontab.guru/) to help create cron expressions
- Make sure each account has a unique `name`
- The configuration file should be placed at `/config.yaml` in production, or in the project root for development
Loading
Loading