[{"data":1,"prerenderedAt":2481},["ShallowReactive",2],{"docs-\u002Fdocs\u002Fcontribute\u002Fintroduction\u002F":3,"docs-nav":2031},{"id":4,"title":5,"body":6,"description":16,"extension":2020,"layout":2021,"meta":2022,"navGroup":2021,"navGroupOrder":2021,"navOrder":203,"navTitle":2023,"navigation":857,"originalPath":2024,"path":2025,"redirect":2021,"seo":2026,"stem":2027,"updated":2028,"version":2029,"__hash__":2030},"docs\u002Fdocs\u002Fcontribute\u002Fintroduction.md","Contributing to FlowFuse",{"type":7,"value":8,"toc":1993},"minimark",[9,13,17,20,25,28,64,68,79,171,175,178,187,190,245,248,255,342,350,354,361,514,519,564,567,570,577,594,601,606,609,623,626,638,641,668,671,693,700,703,716,730,733,741,751,769,772,783,786,789,809,813,816,826,829,1029,1042,1067,1074,1078,1081,1088,1091,1094,1097,1113,1116,1123,1126,1130,1133,1165,1170,1173,1306,1310,1320,1359,1362,1365,1389,1392,1400,1402,1962,1989],[10,11,5],"h1",{"id":12},"contributing-to-flowfuse",[14,15,16],"p",{},"This guide will help you get setup to contribute to the FlowFuse project.",[14,18,19],{},"The core of the FlowFuse platform is available under the Apache-2.0 license and\nwe welcome contributions from the community.",[21,22,24],"h3",{"id":23},"software-requirements","Software Requirements",[14,26,27],{},"This guide assumes you have a working development environment including:",[29,30,31,35,61],"ul",{},[32,33,34],"li",{},"Node.js 18\u002F20",[32,36,37,38],{},"Platform build tools\n",[29,39,40,47,53],{},[32,41,42,43],{},"Linux: ",[44,45,46],"code",{},"apt-get install build-essential",[32,48,49,50],{},"MacOS: ",[44,51,52],{},"xcode-select --install",[32,54,55,56],{},"Windows: installed as part of the official node.js installer\n",[29,57,58],{},[32,59,60],{},"☑️ Automatically install the necessary tools must be checked",[32,62,63],{},"Git",[21,65,67],{"id":66},"project-repositories","Project Repositories",[14,69,70,71,78],{},"There are a number of repositories under the ",[72,73,77],"a",{"href":74,"rel":75},"https:\u002F\u002Fgithub.com\u002FFlowFuse",[76],"nofollow","FlowFuse GitHub organisation","\nthat make up the platform.",[80,81,82,95],"table",{},[83,84,85],"thead",{},[86,87,88,92],"tr",{},[89,90,91],"th",{},"Repository",[89,93,94],{},"Description",[96,97,98,111,123,135,147,159],"tbody",{},[86,99,100,108],{},[101,102,103],"td",{},[72,104,107],{"href":105,"rel":106},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fflowfuse",[76],"FlowFuse",[101,109,110],{},"This is the core of the platform.",[86,112,113,120],{},[101,114,115],{},[72,116,119],{"href":117,"rel":118},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Finstaller",[76],"installer",[101,121,122],{},"The installer for the platform",[86,124,125,132],{},[101,126,127],{},[72,128,131],{"href":129,"rel":130},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fdriver-localfs",[76],"driver-localfs",[101,133,134],{},"The LocalFS driver. This deploys instances to the local system.",[86,136,137,144],{},[101,138,139],{},[72,140,143],{"href":141,"rel":142},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fdriver-docker",[76],"driver-docker",[101,145,146],{},"The Docker driver. This deploys instances as containers in a Docker-managed environment.",[86,148,149,156],{},[101,150,151],{},[72,152,155],{"href":153,"rel":154},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fdriver-k8s",[76],"driver-k8s",[101,157,158],{},"The Kubernetes driver. This deploys instances as containers in a Kubernetes-managed environment.",[86,160,161,168],{},[101,162,163],{},[72,164,167],{"href":165,"rel":166},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnr-launcher",[76],"nr-launcher",[101,169,170],{},"The launcher application is used to start and monitor an individual instance of Node-RED in the FlowFuse platform. This includes a number of Node-RED plugins used to integrate with the FlowFuse platform.",[21,172,174],{"id":173},"setting-up-a-development-environment","Setting Up A Development Environment",[14,176,177],{},"With the project split across multiple repositories, setting up a development\nenvironment manually takes quite a lot of steps to ensure everything is checked\nout and configured properly.",[14,179,180,181,186],{},"To make it easier, you can use the ",[72,182,185],{"href":183,"rel":184},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fdev-env",[76],"FlowFuse Development Environment"," project to get set up.",[14,188,189],{},"The following steps will get your development environment setup in no time:",[191,192,197],"pre",{"className":193,"code":194,"language":195,"meta":196,"style":196},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fdev-env.git\ncd dev-env\nnpm install\nnpm run init\n","bash","",[44,198,199,215,225,234],{"__ignoreMap":196},[200,201,204,208,212],"span",{"class":202,"line":203},"line",1,[200,205,207],{"class":206},"sBMFI","git",[200,209,211],{"class":210},"sfazB"," clone",[200,213,214],{"class":210}," https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fdev-env.git\n",[200,216,218,222],{"class":202,"line":217},2,[200,219,221],{"class":220},"s2Zo4","cd",[200,223,224],{"class":210}," dev-env\n",[200,226,228,231],{"class":202,"line":227},3,[200,229,230],{"class":206},"npm",[200,232,233],{"class":210}," install\n",[200,235,237,239,242],{"class":202,"line":236},4,[200,238,230],{"class":206},[200,240,241],{"class":210}," run",[200,243,244],{"class":210}," init\n",[14,246,247],{},"This clones all of the main project repositories, installs their dependencies and builds\nthe repositories that need it.",[14,249,250,251,254],{},"All of the repositories are cloned under the ",[44,252,253],{},"packages"," directory:",[191,256,260],{"className":257,"code":258,"language":259,"meta":196,"style":196},"language-txt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","dev-env\n└── packages\n    ├── device-agent\n    ├── docker-compose\n    ├── driver-docker\n    ├── driver-k8s\n    ├── driver-localfs\n    ├── file-server\n    ├── flowfuse\n    ├── helm\n    ├── installer\n    ├── nr-file-nodes\n    ├── nr-launcher\n    └── nr-project-nodes\n","txt",[44,261,262,267,272,277,282,288,294,300,306,312,318,324,330,336],{"__ignoreMap":196},[200,263,264],{"class":202,"line":203},[200,265,266],{},"dev-env\n",[200,268,269],{"class":202,"line":217},[200,270,271],{},"└── packages\n",[200,273,274],{"class":202,"line":227},[200,275,276],{},"    ├── device-agent\n",[200,278,279],{"class":202,"line":236},[200,280,281],{},"    ├── docker-compose\n",[200,283,285],{"class":202,"line":284},5,[200,286,287],{},"    ├── driver-docker\n",[200,289,291],{"class":202,"line":290},6,[200,292,293],{},"    ├── driver-k8s\n",[200,295,297],{"class":202,"line":296},7,[200,298,299],{},"    ├── driver-localfs\n",[200,301,303],{"class":202,"line":302},8,[200,304,305],{},"    ├── file-server\n",[200,307,309],{"class":202,"line":308},9,[200,310,311],{},"    ├── flowfuse\n",[200,313,315],{"class":202,"line":314},10,[200,316,317],{},"    ├── helm\n",[200,319,321],{"class":202,"line":320},11,[200,322,323],{},"    ├── installer\n",[200,325,327],{"class":202,"line":326},12,[200,328,329],{},"    ├── nr-file-nodes\n",[200,331,333],{"class":202,"line":332},13,[200,334,335],{},"    ├── nr-launcher\n",[200,337,339],{"class":202,"line":338},14,[200,340,341],{},"    └── nr-project-nodes\n",[14,343,344,345,349],{},"More details on using the FlowForge Development Environment are available in its\n",[72,346,348],{"href":183,"rel":347},[76],"documentation",".",[21,351,353],{"id":352},"flowfuse-code-structure","FlowFuse Code Structure",[14,355,356,357,360],{},"The ",[44,358,359],{},"FlowFuse\u002Fflowfuse"," repository is the core of the platform and where you'll\nlikely want to begin.",[191,362,364],{"className":257,"code":363,"language":259,"meta":196,"style":196},".\n├── bin\n├── config               - build config files\n├── docs\n├── etc                  - FlowFuse platform configuration files\n├── forge                - Platform core code\n│   ├── config\n│   ├── containers\n│   ├── db\n│   ├── ee\n│   ├── lib\n│   ├── licensing\n│   ├── monitor\n│   ├── postoffice\n│   ├── routes\n│   └── settings\n├── frontend             - Frontend code\n│   ├── dist             - build output - created by `npm run build`\n│   ├── public           - static assets\n│   └── src              - vue src\n│       ├── api\n│       ├── components\n│       ├── pages\n│       ├── routes\n│       └── store\n├── test                 - tests for FlowFuse\n└── var                  - where the database and localfs project directories are created\n",[44,365,366,371,376,381,386,391,396,401,406,411,416,421,426,431,436,442,448,454,460,466,472,478,484,490,496,502,508],{"__ignoreMap":196},[200,367,368],{"class":202,"line":203},[200,369,370],{},".\n",[200,372,373],{"class":202,"line":217},[200,374,375],{},"├── bin\n",[200,377,378],{"class":202,"line":227},[200,379,380],{},"├── config               - build config files\n",[200,382,383],{"class":202,"line":236},[200,384,385],{},"├── docs\n",[200,387,388],{"class":202,"line":284},[200,389,390],{},"├── etc                  - FlowFuse platform configuration files\n",[200,392,393],{"class":202,"line":290},[200,394,395],{},"├── forge                - Platform core code\n",[200,397,398],{"class":202,"line":296},[200,399,400],{},"│   ├── config\n",[200,402,403],{"class":202,"line":302},[200,404,405],{},"│   ├── containers\n",[200,407,408],{"class":202,"line":308},[200,409,410],{},"│   ├── db\n",[200,412,413],{"class":202,"line":314},[200,414,415],{},"│   ├── ee\n",[200,417,418],{"class":202,"line":320},[200,419,420],{},"│   ├── lib\n",[200,422,423],{"class":202,"line":326},[200,424,425],{},"│   ├── licensing\n",[200,427,428],{"class":202,"line":332},[200,429,430],{},"│   ├── monitor\n",[200,432,433],{"class":202,"line":338},[200,434,435],{},"│   ├── postoffice\n",[200,437,439],{"class":202,"line":438},15,[200,440,441],{},"│   ├── routes\n",[200,443,445],{"class":202,"line":444},16,[200,446,447],{},"│   └── settings\n",[200,449,451],{"class":202,"line":450},17,[200,452,453],{},"├── frontend             - Frontend code\n",[200,455,457],{"class":202,"line":456},18,[200,458,459],{},"│   ├── dist             - build output - created by `npm run build`\n",[200,461,463],{"class":202,"line":462},19,[200,464,465],{},"│   ├── public           - static assets\n",[200,467,469],{"class":202,"line":468},20,[200,470,471],{},"│   └── src              - vue src\n",[200,473,475],{"class":202,"line":474},21,[200,476,477],{},"│       ├── api\n",[200,479,481],{"class":202,"line":480},22,[200,482,483],{},"│       ├── components\n",[200,485,487],{"class":202,"line":486},23,[200,488,489],{},"│       ├── pages\n",[200,491,493],{"class":202,"line":492},24,[200,494,495],{},"│       ├── routes\n",[200,497,499],{"class":202,"line":498},25,[200,500,501],{},"│       └── store\n",[200,503,505],{"class":202,"line":504},26,[200,506,507],{},"├── test                 - tests for FlowFuse\n",[200,509,511],{"class":202,"line":510},27,[200,512,513],{},"└── var                  - where the database and localfs project directories are created\n",[515,516,518],"h2",{"id":517},"development-setup","Development Setup",[520,521,522,528,534,540,546,552,558],"ol",{},[32,523,524],{},[72,525,527],{"href":526},"#create-a-stack","Create a Stack",[32,529,530],{},[72,531,533],{"href":532},"#running-flowfuse","Running FlowFuse",[32,535,536],{},[72,537,539],{"href":538},"#configuring-flowfuse","Configuring FlowFuse",[32,541,542],{},[72,543,545],{"href":544},"#mocking-email","Mocking email",[32,547,548],{},[72,549,551],{"href":550},"#testing","Testing",[32,553,554],{},[72,555,557],{"href":556},"#vscode-tips","VSCode Tips",[32,559,560],{},[72,561,563],{"href":562},"\u002Fdocs\u002Fcontribute\u002Fteam-broker","Team Broker",[21,565,527],{"id":566},"create-a-stack",[14,568,569],{},"You will need to setup the version(s) of Node-RED you want to use in your stacks.",[14,571,572,573,576],{},"From the ",[44,574,575],{},"flowfuse"," directory run",[191,578,580],{"className":193,"code":579,"language":195,"meta":196,"style":196},"npm run install-stack --vers=3.1.9\n",[44,581,582],{"__ignoreMap":196},[200,583,584,586,588,591],{"class":202,"line":203},[200,585,230],{"class":206},[200,587,241],{"class":210},[200,589,590],{"class":210}," install-stack",[200,592,593],{"class":210}," --vers=3.1.9\n",[14,595,596,597,600],{},"Where ",[44,598,599],{},"3.1.9"," is the version of Node-RED you want to use in the stack.",[602,603,605],"h4",{"id":604},"working-with-local-nodes","Working with Local Nodes",[14,607,608],{},"If you want to test local, in-development nodes in FlowFuse, you can create a dedicated stack for this purpose.",[191,610,611],{"className":193,"code":579,"language":195,"meta":196,"style":196},[44,612,613],{"__ignoreMap":196},[200,614,615,617,619,621],{"class":202,"line":203},[200,616,230],{"class":206},[200,618,241],{"class":210},[200,620,590],{"class":210},[200,622,593],{"class":210},[14,624,625],{},"Navigate to the stacks directory:",[191,627,629],{"className":193,"code":628,"language":195,"meta":196,"style":196},"cd \u002Fvar\u002Fstacks\n",[44,630,631],{"__ignoreMap":196},[200,632,633,635],{"class":202,"line":203},[200,634,221],{"class":220},[200,636,637],{"class":210}," \u002Fvar\u002Fstacks\n",[14,639,640],{},"Rename the directory to something more appropriate:",[191,642,644],{"className":193,"code":643,"language":195,"meta":196,"style":196},"mv \"3.1.9\" \"3.1.9-local\"\n",[44,645,646],{"__ignoreMap":196},[200,647,648,651,655,657,660,662,665],{"class":202,"line":203},[200,649,650],{"class":206},"mv",[200,652,654],{"class":653},"sMK4o"," \"",[200,656,599],{"class":210},[200,658,659],{"class":653},"\"",[200,661,654],{"class":653},[200,663,664],{"class":210},"3.1.9-local",[200,666,667],{"class":653},"\"\n",[14,669,670],{},"Install your local repository directly into the stack:",[191,672,674],{"className":193,"code":673,"language":195,"meta":196,"style":196},"cd 3.1.9-local\nnpm install \u002Fpath\u002Fto\u002Fyour\u002Fnodes-repo\n",[44,675,676,683],{"__ignoreMap":196},[200,677,678,680],{"class":202,"line":203},[200,679,221],{"class":220},[200,681,682],{"class":210}," 3.1.9-local\n",[200,684,685,687,690],{"class":202,"line":217},[200,686,230],{"class":206},[200,688,689],{"class":210}," install",[200,691,692],{"class":210}," \u002Fpath\u002Fto\u002Fyour\u002Fnodes-repo\n",[14,694,695,696,349],{},"With your stack created from the terminal, you can now add it via the FlowFuse Admin UI - see ",[72,697,699],{"href":698},"\u002Fdocs\u002Fadmin\u002Fintroduction\u002F#create-new-stack","Managing Stacks",[21,701,533],{"id":702},"running-flowfuse",[14,704,705,706,708,709,712,713,715],{},"A number of ",[44,707,230],{}," tasks are defined in the ",[44,710,711],{},"package.json"," file of this repository.\nTo get started from the ",[44,714,575],{}," directory use:",[191,717,719],{"className":193,"code":718,"language":195,"meta":196,"style":196},"npm run serve\n",[44,720,721],{"__ignoreMap":196},[200,722,723,725,727],{"class":202,"line":203},[200,724,230],{"class":206},[200,726,241],{"class":210},[200,728,729],{"class":210}," serve\n",[14,731,732],{},"This does a couple of things in parallel:",[29,734,735,738],{},[32,736,737],{},"Starts the core FlowFuse application and watches the source code for any\nchanges - triggering a restart if needed.",[32,739,740],{},"Builds the frontend application using WebPack and watches for any changes - triggering a rebuild as needed.",[14,742,743,744,747,748,349],{},"When running like this, the ",[44,745,746],{},"NODE_ENV"," environment variable gets set to ",[44,749,750],{},"development",[14,752,753,757,758,761,762,765,766,349],{},[754,755,756],"em",{},"Note",": if you have not used the ",[72,759,185],{"href":760},"#setting-up-a-development-environment",", then you will need to run ",[44,763,764],{},"npm run build","\nto build the platform before you can use ",[44,767,768],{},"npm run serve",[21,770,539],{"id":771},"configuring-flowfuse",[14,773,774,775,778,779,782],{},"When running in development mode, the core app will use ",[44,776,777],{},"etc\u002Fflowforge.yml"," for its configuration.\nAs you may want to have a local configuration that you don't want to commit back to git,\nyou can create a file called ",[44,780,781],{},"etc\u002Fflowforge.local.yml"," and it will use that instead.\nThat filename is set to be ignored by git so it won't be accidentally committed.",[21,784,545],{"id":785},"mocking-email",[14,787,788],{},"If you are developing locally and need to enable external email sending, you can either:",[29,790,791,798],{},[32,792,793,794],{},"Setup a local test SMTP server. For example, the Nodemailer project provides a\nuseful app that does the job: ",[72,795,796],{"href":796,"rel":797},"https:\u002F\u002Fgithub.com\u002Fnodemailer\u002Fnodemailer-app\u002Freleases",[76],[32,799,800,801,804,805,808],{},"Alternatively, set the ",[44,802,803],{},"email.debug"," option to ",[44,806,807],{},"true"," in your configuration file\nand the app will print all emails to its log.",[21,810,812],{"id":811},"configuring-billing","Configuring billing",[14,814,815],{},"If you need to develop features covered by the Billing EE feature, you will need\nto configure the platform with a set of valid Stripe API keys and an EE license.",[14,817,356,818,821,822,825],{},[754,819,820],{},"development-only"," EE licence is provided in ",[44,823,824],{},"flowfuse\u002Fforge\u002Flicensing\u002Findex.js",". This\nlicence is not valid for production usage.",[14,827,828],{},"For FlowForge Inc. employees the configuration is provided in 1Password as 'Stripe Testing Configuration'.",[191,830,834],{"className":831,"code":832,"language":833,"meta":196,"style":196},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","license: ***\n\nbilling:\n  stripe:\n    key: ***\n    wh_secret: ***\n    team_price: ***\n    team_product: ***\n    project_price: ***\n    project_product: ***\n    device_price: ***\n    device_product: ***\n    deviceCost: 10\n    new_customer_free_credit: 1000\n    teams:\n      starter:\n        price: ***\n        product: ***\n        userCost: 0\n","yaml",[44,835,836,853,859,867,874,885,896,907,918,929,940,951,962,973,983,990,997,1008,1019],{"__ignoreMap":196},[200,837,838,842,845,849],{"class":202,"line":203},[200,839,841],{"class":840},"swJcz","license",[200,843,844],{"class":653},":",[200,846,848],{"class":847},"s7zQu"," *",[200,850,852],{"class":851},"sTEyZ","**\n",[200,854,855],{"class":202,"line":217},[200,856,858],{"emptyLinePlaceholder":857},true,"\n",[200,860,861,864],{"class":202,"line":227},[200,862,863],{"class":840},"billing",[200,865,866],{"class":653},":\n",[200,868,869,872],{"class":202,"line":236},[200,870,871],{"class":840},"  stripe",[200,873,866],{"class":653},[200,875,876,879,881,883],{"class":202,"line":284},[200,877,878],{"class":840},"    key",[200,880,844],{"class":653},[200,882,848],{"class":847},[200,884,852],{"class":851},[200,886,887,890,892,894],{"class":202,"line":290},[200,888,889],{"class":840},"    wh_secret",[200,891,844],{"class":653},[200,893,848],{"class":847},[200,895,852],{"class":851},[200,897,898,901,903,905],{"class":202,"line":296},[200,899,900],{"class":840},"    team_price",[200,902,844],{"class":653},[200,904,848],{"class":847},[200,906,852],{"class":851},[200,908,909,912,914,916],{"class":202,"line":302},[200,910,911],{"class":840},"    team_product",[200,913,844],{"class":653},[200,915,848],{"class":847},[200,917,852],{"class":851},[200,919,920,923,925,927],{"class":202,"line":308},[200,921,922],{"class":840},"    project_price",[200,924,844],{"class":653},[200,926,848],{"class":847},[200,928,852],{"class":851},[200,930,931,934,936,938],{"class":202,"line":314},[200,932,933],{"class":840},"    project_product",[200,935,844],{"class":653},[200,937,848],{"class":847},[200,939,852],{"class":851},[200,941,942,945,947,949],{"class":202,"line":320},[200,943,944],{"class":840},"    device_price",[200,946,844],{"class":653},[200,948,848],{"class":847},[200,950,852],{"class":851},[200,952,953,956,958,960],{"class":202,"line":326},[200,954,955],{"class":840},"    device_product",[200,957,844],{"class":653},[200,959,848],{"class":847},[200,961,852],{"class":851},[200,963,964,967,969],{"class":202,"line":332},[200,965,966],{"class":840},"    deviceCost",[200,968,844],{"class":653},[200,970,972],{"class":971},"sbssI"," 10\n",[200,974,975,978,980],{"class":202,"line":338},[200,976,977],{"class":840},"    new_customer_free_credit",[200,979,844],{"class":653},[200,981,982],{"class":971}," 1000\n",[200,984,985,988],{"class":202,"line":438},[200,986,987],{"class":840},"    teams",[200,989,866],{"class":653},[200,991,992,995],{"class":202,"line":444},[200,993,994],{"class":840},"      starter",[200,996,866],{"class":653},[200,998,999,1002,1004,1006],{"class":202,"line":450},[200,1000,1001],{"class":840},"        price",[200,1003,844],{"class":653},[200,1005,848],{"class":847},[200,1007,852],{"class":851},[200,1009,1010,1013,1015,1017],{"class":202,"line":456},[200,1011,1012],{"class":840},"        product",[200,1014,844],{"class":653},[200,1016,848],{"class":847},[200,1018,852],{"class":851},[200,1020,1021,1024,1026],{"class":202,"line":462},[200,1022,1023],{"class":840},"        userCost",[200,1025,844],{"class":653},[200,1027,1028],{"class":971}," 0\n",[14,1030,1031,1032,1037,1038,1041],{},"You will also need to install the ",[72,1033,1036],{"href":1034,"rel":1035},"https:\u002F\u002Fstripe.com\u002Fdocs\u002Fcli\u002F",[76],"Stripe CLI"," to handle webhook callbacks properly. Install the CLI following their documentation, then\nrun the following command, with the API key using the value of ",[44,1039,1040],{},"billing.stripe.key"," from\nabove.",[191,1043,1045],{"className":193,"code":1044,"language":195,"meta":196,"style":196},"stripe listen --forward-to localhost:3000\u002Fee\u002Fbilling\u002Fcallback --api-key ***\n",[44,1046,1047],{"__ignoreMap":196},[200,1048,1049,1052,1055,1058,1061,1064],{"class":202,"line":203},[200,1050,1051],{"class":206},"stripe",[200,1053,1054],{"class":210}," listen",[200,1056,1057],{"class":210}," --forward-to",[200,1059,1060],{"class":210}," localhost:3000\u002Fee\u002Fbilling\u002Fcallback",[200,1062,1063],{"class":210}," --api-key",[200,1065,1066],{"class":851}," ***\n",[14,1068,1069,1070,1073],{},"Note that due to the way Stripe works, you will receive events for ",[754,1071,1072],{},"all"," activity\nin the configured Stripe account. That means if someone else is actively developing\nwith billing enabled on the same account, you will see their events arrive.",[602,1075,1077],{"id":1076},"free-trials","Free Trials",[14,1079,1080],{},"Free trials are implemented as a Stripe Credit that is applied when a FlowFuse user\ncreates their first team and completes billing sign up.",[14,1082,1083,1084,1087],{},"To enable trials, set the ",[44,1085,1086],{},"billing.stripe.new_customer_free_credit"," value to a credit amount in cents.\nFor a totally free trial, this amount should match the cost of the Stripe product for the project type to be trialed to be trialed.",[14,1089,1090],{},"The Stripe webhook forwarder must be running as the credit is handled as part of the webhook handling.",[21,1092,551],{"id":1093},"testing",[14,1095,1096],{},"Our testing philosophy follows the principle of:",[1098,1099,1100],"blockquote",{},[14,1101,1102,1103],{},"Write tests. Not too many. Mostly integration ",[1104,1105,1106],"sup",{},[72,1107,1112],{"href":1108,"ariaDescribedBy":1109,"dataFootnoteRef":196,"id":1111},"#user-content-fn-1",[1110],"footnote-label","user-content-fnref-1","1",[14,1114,1115],{},"We create both unit tests and system level tests. The former is suitable for\nwell-contained components that need to provide a stable api and behavior to\nthe rest of the code base. The latter is for testing the external behavior\nof the platform as a whole with as little internal mocking as possible.",[14,1117,1118,1119,1122],{},"We use code coverage reporting as ",[754,1120,1121],{},"one"," aspect of assessing our testing coverage.\nWe do not treat 100% coverage as an imperative goal - that can often lead to\nbusy work writing tests that don't provide any real value in understanding the\noverall quality of the system.",[14,1124,1125],{},"Unit tests should provide sufficient coverage to give us confidence that a\ncomponent's behavior does not unexpectedly change.",[602,1127,1129],{"id":1128},"running-tests","Running tests",[14,1131,1132],{},"To run the tests for the project, you can use the following npm tasks:",[29,1134,1135,1141,1147,1153,1159],{},[32,1136,1137,1140],{},[44,1138,1139],{},"npm run test"," - runs the whole test suite, covering code linting, unit and systems tests.",[32,1142,1143,1146],{},[44,1144,1145],{},"npm run lint"," - runs the linting tests",[32,1148,1149,1152],{},[44,1150,1151],{},"npm run test:unit"," - runs the unit tests",[32,1154,1155,1158],{},[44,1156,1157],{},"npm run test:system"," - runs the system tests",[32,1160,1161,1164],{},[44,1162,1163],{},"npm run test:docs"," - checks the validity of links in the documentation",[1166,1167,1169],"h5",{"id":1168},"testing-against-postgresql","Testing against PostgreSQL",[14,1171,1172],{},"By default, the tests use an in-memory SQLite database to test against. This is\nthe most self-contained way of testing the platform. But it is also necessary to\ntest against PostgreSQL. To enable the use of PostgreSQL in the tests:",[520,1174,1175,1214],{},[32,1176,1177,1178],{},"Ensure you have an instance of PostgreSQL running locally. For example, via docker:",[191,1179,1181],{"className":193,"code":1180,"language":195,"meta":196,"style":196},"   docker run -it -p 5432:5432 --name ff-postgres -e POSTGRES_PASSWORD=secret postgres:14\n",[44,1182,1183],{"__ignoreMap":196},[200,1184,1185,1188,1190,1193,1196,1199,1202,1205,1208,1211],{"class":202,"line":203},[200,1186,1187],{"class":206},"   docker",[200,1189,241],{"class":210},[200,1191,1192],{"class":210}," -it",[200,1194,1195],{"class":210}," -p",[200,1197,1198],{"class":210}," 5432:5432",[200,1200,1201],{"class":210}," --name",[200,1203,1204],{"class":210}," ff-postgres",[200,1206,1207],{"class":210}," -e",[200,1209,1210],{"class":210}," POSTGRES_PASSWORD=secret",[200,1212,1213],{"class":210}," postgres:14\n",[32,1215,1216,1217,1237,1240,1241],{},"Enable PostgreSQL mode by setting the following environment variable:",[191,1218,1220],{"className":193,"code":1219,"language":195,"meta":196,"style":196},"   export FF_TEST_DB_POSTGRES=true\n",[44,1221,1222],{"__ignoreMap":196},[200,1223,1224,1228,1231,1234],{"class":202,"line":203},[200,1225,1227],{"class":1226},"spNyl","   export",[200,1229,1230],{"class":851}," FF_TEST_DB_POSTGRES",[200,1232,1233],{"class":653},"=",[200,1235,1236],{"class":851},"true\n",[1238,1239],"br",{},"The database connection can be set using the following env vars (default values shown)",[191,1242,1244],{"className":193,"code":1243,"language":195,"meta":196,"style":196},"   export FF_TEST_DB_POSTGRES_HOST=localhost\n   export FF_TEST_DB_POSTGRES_PORT=5432\n   export FF_TEST_DB_POSTGRES_USER=postgres\n   export FF_TEST_DB_POSTGRES_PASSWORD=secret\n   export FF_TEST_DB_POSTGRES_DATABASE=flowforge_test\n",[44,1245,1246,1258,1270,1282,1294],{"__ignoreMap":196},[200,1247,1248,1250,1253,1255],{"class":202,"line":203},[200,1249,1227],{"class":1226},[200,1251,1252],{"class":851}," FF_TEST_DB_POSTGRES_HOST",[200,1254,1233],{"class":653},[200,1256,1257],{"class":851},"localhost\n",[200,1259,1260,1262,1265,1267],{"class":202,"line":217},[200,1261,1227],{"class":1226},[200,1263,1264],{"class":851}," FF_TEST_DB_POSTGRES_PORT",[200,1266,1233],{"class":653},[200,1268,1269],{"class":971},"5432\n",[200,1271,1272,1274,1277,1279],{"class":202,"line":227},[200,1273,1227],{"class":1226},[200,1275,1276],{"class":851}," FF_TEST_DB_POSTGRES_USER",[200,1278,1233],{"class":653},[200,1280,1281],{"class":851},"postgres\n",[200,1283,1284,1286,1289,1291],{"class":202,"line":236},[200,1285,1227],{"class":1226},[200,1287,1288],{"class":851}," FF_TEST_DB_POSTGRES_PASSWORD",[200,1290,1233],{"class":653},[200,1292,1293],{"class":851},"secret\n",[200,1295,1296,1298,1301,1303],{"class":202,"line":284},[200,1297,1227],{"class":1226},[200,1299,1300],{"class":851}," FF_TEST_DB_POSTGRES_DATABASE",[200,1302,1233],{"class":653},[200,1304,1305],{"class":851},"flowforge_test\n",[602,1307,1309],{"id":1308},"reporting-code-coverage","Reporting code coverage",[14,1311,356,1312,1315,1316,1319],{},[44,1313,1314],{},"test:*"," tasks have corresponding code coverage tasks. These tasks run the\ntests using ",[44,1317,1318],{},"nyc"," to generate code coverage information.",[29,1321,1322,1332,1342,1350],{},[32,1323,1324,1327,1328,1331],{},[44,1325,1326],{},"npm run cover"," - runs the whole test suite (excluding linting) with code\ncoverage enabled and generates a report (via the ",[44,1329,1330],{},"cover:report"," task)",[32,1333,1334,1337,1338,1341],{},[44,1335,1336],{},"npm run cover:unit"," - runs the unit tests with code coverage enabled. It\ndoes ",[754,1339,1340],{},"not"," generate the report.",[32,1343,1344,1347,1348,1341],{},[44,1345,1346],{},"npm run cover:system"," - runs the system tests with code coverage enabled. It\ndoes ",[754,1349,1340],{},[32,1351,1352,1355,1356],{},[44,1353,1354],{},"npm run cover:report"," - generates a report of the code coverage. This is\nprinted to the console and generates a browsable HTML copy under ",[44,1357,1358],{},"coverage\u002Findex.html",[21,1360,557],{"id":1361},"vscode-tips",[14,1363,1364],{},"To step debug in VSCode",[520,1366,1367,1374,1381],{},[32,1368,1369,1370,1373],{},"Open ",[44,1371,1372],{},"launch.json"," config and enter the JavaScript below",[32,1375,1376,1377,1380],{},"Choose ",[44,1378,1379],{},"Start-Watch"," from the \"Run and Debug\" menu",[32,1382,1383,1384,1388],{},"Press ▶️ or ",[1385,1386,1387],"kbd",{},"F5"," to start debugging",[14,1390,1391],{},"There are 2 other \"Run and Debug\" entries in the menu...",[29,1393,1394,1397],{},[32,1395,1396],{},"\"Attach by Process ID\" - this will allow you to attach to a launched driver",[32,1398,1399],{},"\"Debug Current Test\" - this will enable you to step debug a test (starts debugging the currently open test file)",[602,1401],{"id":196},[191,1403,1407],{"className":1404,"code":1405,"language":1406,"meta":196,"style":196},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n    \u002F\u002F Use IntelliSense to learn about possible attributes.\n    \u002F\u002F Hover to view descriptions of existing attributes.\n    \u002F\u002F For more information, visit: https:\u002F\u002Fgo.microsoft.com\u002Ffwlink\u002F?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"command\": \"npm run start-watch\",\n            \"name\": \"Start-Watch\",\n            \"request\": \"launch\",\n            \"type\": \"node-terminal\",\n            \"env\": {\n                \"NODE_ENV\": \"development\"\n            }\n        },\n        {\n            \"name\": \"Attach by Process ID\",\n            \"processId\": \"${command:PickProcess}\",\n            \"request\": \"attach\",\n            \"skipFiles\": [\n                \"\u003Cnode_internals>\u002F**\"\n            ],\n            \"type\": \"node\"\n        },\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Debug Current Test\",\n            \"program\": \"${workspaceFolder}\u002Fnode_modules\u002Fmocha\u002Fbin\u002F_mocha\",\n            \"args\": [\n              \"--no-warnings\",\n              \"-u\",\n              \"bdd\",\u002F\u002F set to bdd, not tdd\n              \"--timeout\",\n              \"999999\",\n              \"--colors\",\n              \"${file}\"\n            ],\n            \"env\": {\n                \"NODE_ENV\": \"development\"\n            },\n            \"internalConsoleOptions\": \"openOnSessionStart\"\n        }\n    ]\n}\n","json",[44,1408,1409,1414,1420,1425,1430,1452,1466,1471,1492,1511,1531,1551,1565,1582,1587,1592,1596,1615,1635,1654,1667,1676,1681,1698,1702,1706,1724,1742,1762,1783,1797,1810,1822,1838,1850,1862,1874,1884,1889,1902,1919,1925,1944,1950,1956],{"__ignoreMap":196},[200,1410,1411],{"class":202,"line":203},[200,1412,1413],{"class":653},"{\n",[200,1415,1416],{"class":202,"line":217},[200,1417,1419],{"class":1418},"sHwdD","    \u002F\u002F Use IntelliSense to learn about possible attributes.\n",[200,1421,1422],{"class":202,"line":227},[200,1423,1424],{"class":1418},"    \u002F\u002F Hover to view descriptions of existing attributes.\n",[200,1426,1427],{"class":202,"line":236},[200,1428,1429],{"class":1418},"    \u002F\u002F For more information, visit: https:\u002F\u002Fgo.microsoft.com\u002Ffwlink\u002F?linkid=830387\n",[200,1431,1432,1435,1438,1440,1442,1444,1447,1449],{"class":202,"line":284},[200,1433,1434],{"class":653},"    \"",[200,1436,1437],{"class":1226},"version",[200,1439,659],{"class":653},[200,1441,844],{"class":653},[200,1443,654],{"class":653},[200,1445,1446],{"class":210},"0.2.0",[200,1448,659],{"class":653},[200,1450,1451],{"class":653},",\n",[200,1453,1454,1456,1459,1461,1463],{"class":202,"line":290},[200,1455,1434],{"class":653},[200,1457,1458],{"class":1226},"configurations",[200,1460,659],{"class":653},[200,1462,844],{"class":653},[200,1464,1465],{"class":653}," [\n",[200,1467,1468],{"class":202,"line":296},[200,1469,1470],{"class":653},"        {\n",[200,1472,1473,1476,1479,1481,1483,1485,1488,1490],{"class":202,"line":302},[200,1474,1475],{"class":653},"            \"",[200,1477,1478],{"class":206},"command",[200,1480,659],{"class":653},[200,1482,844],{"class":653},[200,1484,654],{"class":653},[200,1486,1487],{"class":210},"npm run start-watch",[200,1489,659],{"class":653},[200,1491,1451],{"class":653},[200,1493,1494,1496,1499,1501,1503,1505,1507,1509],{"class":202,"line":308},[200,1495,1475],{"class":653},[200,1497,1498],{"class":206},"name",[200,1500,659],{"class":653},[200,1502,844],{"class":653},[200,1504,654],{"class":653},[200,1506,1379],{"class":210},[200,1508,659],{"class":653},[200,1510,1451],{"class":653},[200,1512,1513,1515,1518,1520,1522,1524,1527,1529],{"class":202,"line":314},[200,1514,1475],{"class":653},[200,1516,1517],{"class":206},"request",[200,1519,659],{"class":653},[200,1521,844],{"class":653},[200,1523,654],{"class":653},[200,1525,1526],{"class":210},"launch",[200,1528,659],{"class":653},[200,1530,1451],{"class":653},[200,1532,1533,1535,1538,1540,1542,1544,1547,1549],{"class":202,"line":320},[200,1534,1475],{"class":653},[200,1536,1537],{"class":206},"type",[200,1539,659],{"class":653},[200,1541,844],{"class":653},[200,1543,654],{"class":653},[200,1545,1546],{"class":210},"node-terminal",[200,1548,659],{"class":653},[200,1550,1451],{"class":653},[200,1552,1553,1555,1558,1560,1562],{"class":202,"line":326},[200,1554,1475],{"class":653},[200,1556,1557],{"class":206},"env",[200,1559,659],{"class":653},[200,1561,844],{"class":653},[200,1563,1564],{"class":653}," {\n",[200,1566,1567,1570,1572,1574,1576,1578,1580],{"class":202,"line":332},[200,1568,1569],{"class":653},"                \"",[200,1571,746],{"class":971},[200,1573,659],{"class":653},[200,1575,844],{"class":653},[200,1577,654],{"class":653},[200,1579,750],{"class":210},[200,1581,667],{"class":653},[200,1583,1584],{"class":202,"line":338},[200,1585,1586],{"class":653},"            }\n",[200,1588,1589],{"class":202,"line":438},[200,1590,1591],{"class":653},"        },\n",[200,1593,1594],{"class":202,"line":444},[200,1595,1470],{"class":653},[200,1597,1598,1600,1602,1604,1606,1608,1611,1613],{"class":202,"line":450},[200,1599,1475],{"class":653},[200,1601,1498],{"class":206},[200,1603,659],{"class":653},[200,1605,844],{"class":653},[200,1607,654],{"class":653},[200,1609,1610],{"class":210},"Attach by Process ID",[200,1612,659],{"class":653},[200,1614,1451],{"class":653},[200,1616,1617,1619,1622,1624,1626,1628,1631,1633],{"class":202,"line":456},[200,1618,1475],{"class":653},[200,1620,1621],{"class":206},"processId",[200,1623,659],{"class":653},[200,1625,844],{"class":653},[200,1627,654],{"class":653},[200,1629,1630],{"class":210},"${command:PickProcess}",[200,1632,659],{"class":653},[200,1634,1451],{"class":653},[200,1636,1637,1639,1641,1643,1645,1647,1650,1652],{"class":202,"line":462},[200,1638,1475],{"class":653},[200,1640,1517],{"class":206},[200,1642,659],{"class":653},[200,1644,844],{"class":653},[200,1646,654],{"class":653},[200,1648,1649],{"class":210},"attach",[200,1651,659],{"class":653},[200,1653,1451],{"class":653},[200,1655,1656,1658,1661,1663,1665],{"class":202,"line":468},[200,1657,1475],{"class":653},[200,1659,1660],{"class":206},"skipFiles",[200,1662,659],{"class":653},[200,1664,844],{"class":653},[200,1666,1465],{"class":653},[200,1668,1669,1671,1674],{"class":202,"line":474},[200,1670,1569],{"class":653},[200,1672,1673],{"class":210},"\u003Cnode_internals>\u002F**",[200,1675,667],{"class":653},[200,1677,1678],{"class":202,"line":480},[200,1679,1680],{"class":653},"            ],\n",[200,1682,1683,1685,1687,1689,1691,1693,1696],{"class":202,"line":486},[200,1684,1475],{"class":653},[200,1686,1537],{"class":206},[200,1688,659],{"class":653},[200,1690,844],{"class":653},[200,1692,654],{"class":653},[200,1694,1695],{"class":210},"node",[200,1697,667],{"class":653},[200,1699,1700],{"class":202,"line":492},[200,1701,1591],{"class":653},[200,1703,1704],{"class":202,"line":498},[200,1705,1470],{"class":653},[200,1707,1708,1710,1712,1714,1716,1718,1720,1722],{"class":202,"line":504},[200,1709,1475],{"class":653},[200,1711,1537],{"class":206},[200,1713,659],{"class":653},[200,1715,844],{"class":653},[200,1717,654],{"class":653},[200,1719,1695],{"class":210},[200,1721,659],{"class":653},[200,1723,1451],{"class":653},[200,1725,1726,1728,1730,1732,1734,1736,1738,1740],{"class":202,"line":510},[200,1727,1475],{"class":653},[200,1729,1517],{"class":206},[200,1731,659],{"class":653},[200,1733,844],{"class":653},[200,1735,654],{"class":653},[200,1737,1526],{"class":210},[200,1739,659],{"class":653},[200,1741,1451],{"class":653},[200,1743,1745,1747,1749,1751,1753,1755,1758,1760],{"class":202,"line":1744},28,[200,1746,1475],{"class":653},[200,1748,1498],{"class":206},[200,1750,659],{"class":653},[200,1752,844],{"class":653},[200,1754,654],{"class":653},[200,1756,1757],{"class":210},"Debug Current Test",[200,1759,659],{"class":653},[200,1761,1451],{"class":653},[200,1763,1765,1767,1770,1772,1774,1776,1779,1781],{"class":202,"line":1764},29,[200,1766,1475],{"class":653},[200,1768,1769],{"class":206},"program",[200,1771,659],{"class":653},[200,1773,844],{"class":653},[200,1775,654],{"class":653},[200,1777,1778],{"class":210},"${workspaceFolder}\u002Fnode_modules\u002Fmocha\u002Fbin\u002F_mocha",[200,1780,659],{"class":653},[200,1782,1451],{"class":653},[200,1784,1786,1788,1791,1793,1795],{"class":202,"line":1785},30,[200,1787,1475],{"class":653},[200,1789,1790],{"class":206},"args",[200,1792,659],{"class":653},[200,1794,844],{"class":653},[200,1796,1465],{"class":653},[200,1798,1800,1803,1806,1808],{"class":202,"line":1799},31,[200,1801,1802],{"class":653},"              \"",[200,1804,1805],{"class":210},"--no-warnings",[200,1807,659],{"class":653},[200,1809,1451],{"class":653},[200,1811,1813,1815,1818,1820],{"class":202,"line":1812},32,[200,1814,1802],{"class":653},[200,1816,1817],{"class":210},"-u",[200,1819,659],{"class":653},[200,1821,1451],{"class":653},[200,1823,1825,1827,1830,1832,1835],{"class":202,"line":1824},33,[200,1826,1802],{"class":653},[200,1828,1829],{"class":210},"bdd",[200,1831,659],{"class":653},[200,1833,1834],{"class":653},",",[200,1836,1837],{"class":1418},"\u002F\u002F set to bdd, not tdd\n",[200,1839,1841,1843,1846,1848],{"class":202,"line":1840},34,[200,1842,1802],{"class":653},[200,1844,1845],{"class":210},"--timeout",[200,1847,659],{"class":653},[200,1849,1451],{"class":653},[200,1851,1853,1855,1858,1860],{"class":202,"line":1852},35,[200,1854,1802],{"class":653},[200,1856,1857],{"class":210},"999999",[200,1859,659],{"class":653},[200,1861,1451],{"class":653},[200,1863,1865,1867,1870,1872],{"class":202,"line":1864},36,[200,1866,1802],{"class":653},[200,1868,1869],{"class":210},"--colors",[200,1871,659],{"class":653},[200,1873,1451],{"class":653},[200,1875,1877,1879,1882],{"class":202,"line":1876},37,[200,1878,1802],{"class":653},[200,1880,1881],{"class":210},"${file}",[200,1883,667],{"class":653},[200,1885,1887],{"class":202,"line":1886},38,[200,1888,1680],{"class":653},[200,1890,1892,1894,1896,1898,1900],{"class":202,"line":1891},39,[200,1893,1475],{"class":653},[200,1895,1557],{"class":206},[200,1897,659],{"class":653},[200,1899,844],{"class":653},[200,1901,1564],{"class":653},[200,1903,1905,1907,1909,1911,1913,1915,1917],{"class":202,"line":1904},40,[200,1906,1569],{"class":653},[200,1908,746],{"class":971},[200,1910,659],{"class":653},[200,1912,844],{"class":653},[200,1914,654],{"class":653},[200,1916,750],{"class":210},[200,1918,667],{"class":653},[200,1920,1922],{"class":202,"line":1921},41,[200,1923,1924],{"class":653},"            },\n",[200,1926,1928,1930,1933,1935,1937,1939,1942],{"class":202,"line":1927},42,[200,1929,1475],{"class":653},[200,1931,1932],{"class":206},"internalConsoleOptions",[200,1934,659],{"class":653},[200,1936,844],{"class":653},[200,1938,654],{"class":653},[200,1940,1941],{"class":210},"openOnSessionStart",[200,1943,667],{"class":653},[200,1945,1947],{"class":202,"line":1946},43,[200,1948,1949],{"class":653},"        }\n",[200,1951,1953],{"class":202,"line":1952},44,[200,1954,1955],{"class":653},"    ]\n",[200,1957,1959],{"class":202,"line":1958},45,[200,1960,1961],{"class":653},"}\n",[1963,1964,1967,1972],"section",{"className":1965,"dataFootnotes":196},[1966],"footnotes",[515,1968,1971],{"className":1969,"id":1110},[1970],"sr-only","Footnotes",[520,1973,1974],{},[32,1975,1977,1981,1982],{"id":1976},"user-content-fn-1",[72,1978,1979],{"href":1979,"rel":1980},"https:\u002F\u002Fkentcdodds.com\u002Fblog\u002Fwrite-tests",[76]," ",[72,1983,1988],{"href":1984,"ariaLabel":1985,"className":1986,"dataFootnoteBackref":196},"#user-content-fnref-1","Back to reference 1",[1987],"data-footnote-backref","↩",[1990,1991,1992],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":196,"searchDepth":236,"depth":236,"links":1994},[1995,1996,1997,1998,1999,2019],{"id":23,"depth":227,"text":24},{"id":66,"depth":227,"text":67},{"id":173,"depth":227,"text":174},{"id":352,"depth":227,"text":353},{"id":517,"depth":217,"text":518,"children":2000},[2001,2004,2005,2006,2007,2010,2016],{"id":566,"depth":227,"text":527,"children":2002},[2003],{"id":604,"depth":236,"text":605},{"id":702,"depth":227,"text":533},{"id":771,"depth":227,"text":539},{"id":785,"depth":227,"text":545},{"id":811,"depth":227,"text":812,"children":2008},[2009],{"id":1076,"depth":236,"text":1077},{"id":1093,"depth":227,"text":551,"children":2011},[2012,2015],{"id":1128,"depth":236,"text":1129,"children":2013},[2014],{"id":1168,"depth":284,"text":1169},{"id":1308,"depth":236,"text":1309},{"id":1361,"depth":227,"text":557,"children":2017},[2018],{"id":196,"depth":236,"text":196},{"id":1110,"depth":217,"text":1971},"md",null,{},"Introduction","contribute\u002Fintroduction.md","\u002Fdocs\u002Fcontribute\u002Fintroduction",{"title":5,"description":16},"docs\u002Fcontribute\u002Fintroduction","2025-05-09 10:20:24 +0100","2.33.2","rEzxDuRwjZEU7GaFxT37ypLVHZsoffs2dgI8FuTEBO0",[2032,2039,2058,2069],{"name":2033,"order":203,"children":2034},"FlowFuse User Manuals",[2035],{"title":2036,"path":2037,"group":2033,"groupOrder":203,"order":217,"children":2038},"FlowFuse API","\u002Fdocs\u002Fapi",[],{"name":2040,"order":236,"children":2041},"FlowFuse Self-Hosted",[2042,2046],{"title":2043,"path":2044,"group":2040,"groupOrder":236,"order":203,"children":2045},"Quick Start","\u002Fdocs\u002Fquick-start",[],{"title":2047,"path":2048,"group":2040,"groupOrder":236,"order":227,"children":2049},"Upgrading FlowFuse","\u002Fdocs\u002Fupgrade",[2050,2054],{"title":2051,"path":2052,"order":2021,"children":2053},"Installing a license","\u002Fdocs\u002Fupgrade\u002Fopen-source-to-premium",[],{"title":2055,"path":2056,"order":2021,"children":2057},"Upgrading the Node-RED version","\u002Fdocs\u002Fupgrade\u002Fnodered-version",[],{"name":2059,"order":284,"children":2060},"Support",[2061,2065],{"title":2062,"path":2063,"group":2059,"groupOrder":284,"order":227,"children":2064},"Premium Support","\u002Fdocs\u002Fpremium-support",[],{"title":2066,"path":2067,"group":2059,"groupOrder":284,"order":2021,"children":2068},"Debugging Node-RED issues","\u002Fdocs\u002Fdebugging",[],{"name":2070,"order":2021,"children":2071},"Other",[2072,2120,2131,2207,2255,2274,2347,2358],{"title":2073,"path":2074,"order":2021,"children":2075},"admin","\u002Fdocs\u002Fadmin",[2076,2080,2092,2100,2104,2108,2112,2116],{"title":2077,"path":2078,"order":203,"children":2079},"Administering FlowFuse","\u002Fdocs\u002Fadmin\u002Fintroduction",[],{"title":2081,"path":2082,"order":2021,"children":2083},"Configuring Single Sign-On (SSO)","\u002Fdocs\u002Fadmin\u002Fsso",[2084,2088],{"title":2085,"path":2086,"order":2021,"children":2087},"LDAP SSO","\u002Fdocs\u002Fadmin\u002Fsso\u002Fldap",[],{"title":2089,"path":2090,"order":2021,"children":2091},"SAML SSO","\u002Fdocs\u002Fadmin\u002Fsso\u002Fsaml",[],{"title":2093,"path":2094,"order":2021,"children":2095},"hardening","\u002Fdocs\u002Fadmin\u002Fhardening",[2096],{"title":2097,"path":2098,"order":2021,"children":2099},"Kubernetes Hardening","\u002Fdocs\u002Fadmin\u002Fhardening\u002Fkubernetes",[],{"title":2101,"path":2102,"order":2021,"children":2103},"Monitoring","\u002Fdocs\u002Fadmin\u002Fmonitoring",[],{"title":2105,"path":2106,"order":2021,"children":2107},"Observability","\u002Fdocs\u002Fadmin\u002Fobservability",[],{"title":2109,"path":2110,"order":2021,"children":2111},"Soft Launch Enablement","\u002Fdocs\u002Fadmin\u002Ffeature-flags",[],{"title":2113,"path":2114,"order":2021,"children":2115},"Telemetry","\u002Fdocs\u002Fadmin\u002Ftelemetry",[],{"title":2117,"path":2118,"order":2021,"children":2119},"User Management","\u002Fdocs\u002Fadmin\u002Fuser-management",[],{"title":2121,"path":2122,"order":2021,"children":2123},"cloud","\u002Fdocs\u002Fcloud",[2124,2127],{"title":2023,"path":2125,"order":203,"children":2126},"\u002Fdocs\u002Fcloud\u002Fintroduction",[],{"title":2128,"path":2129,"order":2021,"children":2130},"FlowFuse Cloud Billing","\u002Fdocs\u002Fcloud\u002Fbilling",[],{"title":2132,"path":2133,"order":2021,"children":2134},"contribute","\u002Fdocs\u002Fcontribute",[2135,2137,2141,2145,2149,2153,2157,2165,2201,2203],{"title":2023,"path":2025,"order":203,"children":2136},[],{"title":2138,"path":2139,"order":2021,"children":2140},"Adding Template Settings","\u002Fdocs\u002Fcontribute\u002Fadding-template-settings",[],{"title":2142,"path":2143,"order":2021,"children":2144},"API Design","\u002Fdocs\u002Fcontribute\u002Fapi-design",[],{"title":2146,"path":2147,"order":2021,"children":2148},"Creating debug stack containers","\u002Fdocs\u002Fcontribute\u002Fcreating-debug-stack-containers",[],{"title":2150,"path":2151,"order":2021,"children":2152},"Database migrations","\u002Fdocs\u002Fcontribute\u002Fdb-migrations",[],{"title":2154,"path":2155,"order":2021,"children":2156},"FlowFuse Architecture","\u002Fdocs\u002Fcontribute\u002Farchitecture",[],{"title":2158,"path":2159,"order":2021,"children":2160},"Local Install","\u002Fdocs\u002Fcontribute\u002Flocal",[2161],{"title":2162,"path":2163,"order":2021,"children":2164},"Local Stacks","\u002Fdocs\u002Fcontribute\u002Flocal\u002Fstacks",[],{"title":2166,"path":2167,"order":2021,"children":2168},"State Flows","\u002Fdocs\u002Fcontribute\u002Fworkflows",[2169,2173,2177,2181,2185,2189,2193,2197],{"title":2170,"path":2171,"order":2021,"children":2172},"Device Editor","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Fdevice-editor",[],{"title":2174,"path":2175,"order":2021,"children":2176},"Instance states","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Fproject-states",[],{"title":2178,"path":2179,"order":2021,"children":2180},"Invite External Users","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Finvite-external-user",[],{"title":2182,"path":2183,"order":2021,"children":2184},"Project Creation","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Fproject-create",[],{"title":2186,"path":2187,"order":2021,"children":2188},"Reset Password Flow","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Fpassword-reset",[],{"title":2190,"path":2191,"order":2021,"children":2192},"Team creation Flow","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Fteam-create",[],{"title":2194,"path":2195,"order":2021,"children":2196},"User Login Flows","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Flogin",[],{"title":2198,"path":2199,"order":2021,"children":2200},"User Sign up Flow","\u002Fdocs\u002Fcontribute\u002Fworkflows\u002Fsignup",[],{"title":563,"path":562,"order":2021,"children":2202},[],{"title":2204,"path":2205,"order":2021,"children":2206},"Working with Feature Flags","\u002Fdocs\u002Fcontribute\u002Ffeature-flags",[],{"title":2208,"path":2209,"order":2021,"children":2210},"device-agent","\u002Fdocs\u002Fdevice-agent",[2211,2215,2218,2222,2226,2230],{"title":2212,"path":2213,"order":203,"children":2214},"FlowFuse Device Agent Introduction","\u002Fdocs\u002Fdevice-agent\u002Fintroduction",[],{"title":2043,"path":2216,"order":217,"children":2217},"\u002Fdocs\u002Fdevice-agent\u002Fquickstart",[],{"title":2219,"path":2220,"order":236,"children":2221},"Register your Remote Instance","\u002Fdocs\u002Fdevice-agent\u002Fregister",[],{"title":2223,"path":2224,"order":284,"children":2225},"Running the Agent","\u002Fdocs\u002Fdevice-agent\u002Frunning",[],{"title":2227,"path":2228,"order":290,"children":2229},"Deploying your Flows","\u002Fdocs\u002Fdevice-agent\u002Fdeploy",[],{"title":2231,"path":2232,"order":2021,"children":2233},"install","\u002Fdocs\u002Fdevice-agent\u002Finstall",[2234,2239,2243,2247,2251],{"title":2235,"path":2236,"group":2237,"order":203,"children":2238},"Overview","\u002Fdocs\u002Fdevice-agent\u002Finstall\u002Foverview","DeviceAgentInstallation",[],{"title":2240,"path":2241,"group":2237,"order":217,"children":2242},"Device Agent Installer","\u002Fdocs\u002Fdevice-agent\u002Finstall\u002Fdevice-agent-installer",[],{"title":2244,"path":2245,"group":2237,"order":227,"children":2246},"Docker Install","\u002Fdocs\u002Fdevice-agent\u002Finstall\u002Fdocker",[],{"title":2248,"path":2249,"group":2237,"order":236,"children":2250},"Kubernetes Install","\u002Fdocs\u002Fdevice-agent\u002Finstall\u002Fkubernetes",[],{"title":2252,"path":2253,"group":2237,"order":284,"children":2254},"Manual Install with NPM","\u002Fdocs\u002Fdevice-agent\u002Finstall\u002Fmanual",[],{"title":2256,"path":2257,"order":2021,"children":2258},"hardware","\u002Fdocs\u002Fhardware",[2259,2262,2266,2270],{"title":2235,"path":2260,"order":203,"children":2261},"\u002Fdocs\u002Fhardware\u002Fintroduction",[],{"title":2263,"path":2264,"order":217,"children":2265},"ctrlX - Node-RED","\u002Fdocs\u002Fhardware\u002Fctrlx-node-red",[],{"title":2267,"path":2268,"order":227,"children":2269},"ctrlX - Device Agent","\u002Fdocs\u002Fhardware\u002Fctrlx-device-agent",[],{"title":2271,"path":2272,"order":227,"children":2273},"Raspberry Pi with Raspbian","\u002Fdocs\u002Fhardware\u002Fraspbian",[],{"title":2231,"path":2275,"order":2021,"children":2276},"\u002Fdocs\u002Finstall",[2277,2280,2283,2287,2307,2311,2315,2319],{"title":2235,"path":2278,"order":203,"children":2279},"\u002Fdocs\u002Finstall\u002Fintroduction",[],{"title":539,"path":2281,"order":2021,"children":2282},"\u002Fdocs\u002Finstall\u002Fconfiguration",[],{"title":2284,"path":2285,"order":2021,"children":2286},"DNS Setup","\u002Fdocs\u002Finstall\u002Fdns-setup",[],{"title":2288,"path":2289,"order":2021,"children":2290},"Docker install","\u002Fdocs\u002Finstall\u002Fdocker",[2291,2295,2299,2303],{"title":2292,"path":2293,"order":2021,"children":2294},"Add Project Stacks on Docker","\u002Fdocs\u002Finstall\u002Fdocker\u002Fstacks",[],{"title":2296,"path":2297,"order":2021,"children":2298},"Docker Engine on Windows","\u002Fdocs\u002Finstall\u002Fdocker\u002Fwindows-docker-ce",[],{"title":2300,"path":2301,"order":2021,"children":2302},"Docker from AWS Market Place","\u002Fdocs\u002Finstall\u002Fdocker\u002Faws-marketplace",[],{"title":2304,"path":2305,"order":2021,"children":2306},"Docker on Digital Ocean","\u002Fdocs\u002Finstall\u002Fdocker\u002Fdigital-ocean",[],{"title":2308,"path":2309,"order":2021,"children":2310},"Email configuration","\u002Fdocs\u002Finstall\u002Femail-providers",[],{"title":2312,"path":2313,"order":2021,"children":2314},"First Run Setup","\u002Fdocs\u002Finstall\u002Ffirst-run",[],{"title":2316,"path":2317,"order":2021,"children":2318},"FlowFuse File Storage","\u002Fdocs\u002Finstall\u002Ffile-storage",[],{"title":2320,"path":2321,"order":2021,"children":2322},"Install FlowFuse on Kubernetes","\u002Fdocs\u002Finstall\u002Fkubernetes",[2323,2327,2331,2335,2339,2343],{"title":2324,"path":2325,"order":2021,"children":2326},"AWS EKS Installation","\u002Fdocs\u002Finstall\u002Fkubernetes\u002Faws",[],{"title":2328,"path":2329,"order":2021,"children":2330},"AWS EKS Installation with Terraform and Helm","\u002Fdocs\u002Finstall\u002Fkubernetes\u002Faws-terraform",[],{"title":2332,"path":2333,"order":2021,"children":2334},"Digital Ocean Kubernetes Installation","\u002Fdocs\u002Finstall\u002Fkubernetes\u002Fdigital-ocean",[],{"title":2336,"path":2337,"order":2021,"children":2338},"Ingress Controller Migration","\u002Fdocs\u002Finstall\u002Fkubernetes\u002Fingress-controller-migration",[],{"title":2340,"path":2341,"order":2021,"children":2342},"Kubernetes Project Stacks","\u002Fdocs\u002Finstall\u002Fkubernetes\u002Fstacks",[],{"title":2344,"path":2345,"order":2021,"children":2346},"OpenShift Installation","\u002Fdocs\u002Finstall\u002Fkubernetes\u002Fopenshift",[],{"title":2348,"path":2349,"order":2021,"children":2350},"migration","\u002Fdocs\u002Fmigration",[2351,2354],{"title":2023,"path":2352,"order":203,"children":2353},"\u002Fdocs\u002Fmigration\u002Fintroduction",[],{"title":2355,"path":2356,"order":2021,"children":2357},"Node-RED Tools plugin","\u002Fdocs\u002Fmigration\u002Fnode-red-tools",[],{"title":2359,"path":2360,"order":2021,"children":2361},"user","\u002Fdocs\u002Fuser",[2362,2366,2370,2374,2378,2382,2386,2390,2394,2398,2402,2406,2418,2422,2426,2430,2434,2438,2442,2446,2450,2454,2458,2462,2466,2470,2473,2477],{"title":2363,"path":2364,"order":203,"children":2365},"Getting Started","\u002Fdocs\u002Fuser\u002Fintroduction",[],{"title":2367,"path":2368,"order":203,"children":2369},"Static asset service","\u002Fdocs\u002Fuser\u002Fstatic-asset-service",[],{"title":2371,"path":2372,"order":217,"children":2373},"Bill of Materials","\u002Fdocs\u002Fuser\u002Fbill-of-materials",[],{"title":2375,"path":2376,"order":217,"children":2377},"FlowFuse Concepts","\u002Fdocs\u002Fuser\u002Fconcepts",[],{"title":2379,"path":2380,"order":314,"children":2381},"Instance States","\u002Fdocs\u002Fuser\u002Finstance-states",[],{"title":2383,"path":2384,"order":2021,"children":2385},"Changing the Stack","\u002Fdocs\u002Fuser\u002Fchangestack",[],{"title":2387,"path":2388,"order":2021,"children":2389},"Custom Hostnames","\u002Fdocs\u002Fuser\u002Fcustom-hostnames",[],{"title":2391,"path":2392,"order":2021,"children":2393},"Custom Node Packages","\u002Fdocs\u002Fuser\u002Fcustom-npm-packages",[],{"title":2395,"path":2396,"order":2021,"children":2397},"Dashboards","\u002Fdocs\u002Fuser\u002Fdashboards",[],{"title":2399,"path":2400,"order":2021,"children":2401},"DevOps Pipelines","\u002Fdocs\u002Fuser\u002Fdevops-pipelines",[],{"title":2403,"path":2404,"order":2021,"children":2405},"Environment Variables","\u002Fdocs\u002Fuser\u002Fenvvar",[],{"title":2407,"path":2408,"order":2021,"children":2409},"FlowFuse Expert","\u002Fdocs\u002Fuser\u002Fexpert",[2410,2414],{"title":2411,"path":2412,"order":2021,"children":2413},"AI in Node-RED","\u002Fdocs\u002Fuser\u002Fexpert\u002Fnode-red-embedded-ai",[],{"title":2415,"path":2416,"order":2021,"children":2417},"Chat Interface","\u002Fdocs\u002Fuser\u002Fexpert\u002Fchat",[],{"title":2419,"path":2420,"order":2021,"children":2421},"FlowFuse File Nodes","\u002Fdocs\u002Fuser\u002Ffilenodes",[],{"title":2423,"path":2424,"order":2021,"children":2425},"FlowFuse MQTT Nodes","\u002Fdocs\u002Fuser\u002Fmqtt-nodes",[],{"title":2427,"path":2428,"order":2021,"children":2429},"FlowFuse Persistent Context","\u002Fdocs\u002Fuser\u002Fpersistent-context",[],{"title":2431,"path":2432,"order":2021,"children":2433},"FlowFuse Project Nodes","\u002Fdocs\u002Fuser\u002Fprojectnodes",[],{"title":2435,"path":2436,"order":2021,"children":2437},"FlowFuse Tables","\u002Fdocs\u002Fuser\u002Fff-tables",[],{"title":2439,"path":2440,"order":2021,"children":2441},"Groups","\u002Fdocs\u002Fuser\u002Fdevice-groups",[],{"title":2443,"path":2444,"order":2021,"children":2445},"High Availability mode","\u002Fdocs\u002Fuser\u002Fhigh-availability",[],{"title":2447,"path":2448,"order":2021,"children":2449},"HTTP Access Tokens","\u002Fdocs\u002Fuser\u002Fhttp-access-tokens",[],{"title":2451,"path":2452,"order":2021,"children":2453},"Instance Settings","\u002Fdocs\u002Fuser\u002Finstance-settings",[],{"title":2455,"path":2456,"order":2021,"children":2457},"Logging","\u002Fdocs\u002Fuser\u002Flogs",[],{"title":2459,"path":2460,"order":2021,"children":2461},"Role-Based Access Control","\u002Fdocs\u002Fuser\u002Frole-based-access-control",[],{"title":2463,"path":2464,"order":2021,"children":2465},"Shared Team Library","\u002Fdocs\u002Fuser\u002Fshared-library",[],{"title":2467,"path":2468,"order":2021,"children":2469},"Snapshots","\u002Fdocs\u002Fuser\u002Fsnapshots",[],{"title":563,"path":2471,"order":2021,"children":2472},"\u002Fdocs\u002Fuser\u002Fteambroker",[],{"title":2474,"path":2475,"order":2021,"children":2476},"Teams","\u002Fdocs\u002Fuser\u002Fteam",[],{"title":2478,"path":2479,"order":2021,"children":2480},"User Settings","\u002Fdocs\u002Fuser\u002Fuser-settings",[],1786546037869]