[{"data":1,"prerenderedAt":2113},["ShallowReactive",2],{"docs:/docs/self-hosting/project-document":3},{"id":4,"title":5,"body":6,"description":2105,"extension":2106,"meta":2107,"navigation":2108,"path":2109,"seo":2110,"stem":2111,"__hash__":2112},"docs/docs/self-hosting/project-document.md","Project Document",{"type":7,"value":8,"toc":2090},"minimark",[9,13,21,33,38,786,789,858,862,865,908,919,1027,1030,1033,1045,1051,1068,1072,1078,1098,1103,1209,1213,1306,1319,1336,1342,1394,1399,1531,1537,1611,1631,1637,1678,1685,1690,1827,1848,1854,1883,1889,1893,1904,2059,2079,2086],[10,11,5],"h1",{"id":12},"project-document",[14,15,16,20],"p",{},[17,18,19],"code",{},"@nolag/core"," creates a project from a single JSON document and can export any project back to the same format. Everything in it is addressed by slug rather than by id, so a document exported from one deployment imports cleanly into another.",[14,22,23,24,28,29,32],{},"An import always creates a ",[25,26,27],"strong",{},"new"," project and returns the credentials it minted, once. An export contains ",[25,30,31],{},"no"," secrets, so it is safe to keep in version control; importing it again mints fresh credentials.",[34,35,37],"h2",{"id":36},"an-example","An example",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-json shiki shiki-themes github-light github-dark","{\n  \"version\": 1,\n  \"project\": {\n    \"name\": \"Fleet\",\n    \"description\": \"Vehicles, dispatchers and a customer-facing web app.\",\n    \"limits\": { \"maxConnections\": 500 }\n  },\n  \"accessScopes\": [\n    { \"slug\": \"acme\", \"name\": \"Acme Logistics\" }\n  ],\n  \"apps\": [\n    {\n      \"slug\": \"fleet\",\n      \"name\": \"Fleet\",\n      \"accessMode\": \"restricted\",\n      \"topics\": [\"location\", \"status\", \"commands\"],\n      \"triggerWebhook\": {\n        \"url\": \"https://hooks.example.com/nolag/fleet\",\n        \"headers\": { \"Authorization\": \"Bearer replace-me\" }\n      },\n      \"rooms\": [\n        { \"slug\": \"north\", \"name\": \"North depot\" },\n        { \"slug\": \"south\", \"name\": \"South depot\" },\n        {\n          \"slug\": \"dispatch\",\n          \"name\": \"Dispatch\",\n          \"typeGrants\": [{ \"actorType\": \"service\", \"permission\": \"pubSub\" }]\n        }\n      ],\n      \"lobbies\": [\n        { \"slug\": \"all-depots\", \"name\": \"All depots\", \"rooms\": [\"north\", \"south\"] }\n      ]\n    }\n  ],\n  \"actors\": [\n    {\n      \"ref\": \"dispatcher\",\n      \"name\": \"Dispatch service\",\n      \"actorType\": \"service\",\n      \"appAccess\": [{ \"appSlug\": \"fleet\", \"permission\": \"pubSub\" }]\n    },\n    {\n      \"ref\": \"truck-17\",\n      \"name\": \"Truck 17\",\n      \"actorType\": \"device\",\n      \"appAccess\": [{ \"appSlug\": \"fleet\", \"permission\": \"publish\", \"topics\": [\"location\", \"status\"] }]\n    },\n    {\n      \"ref\": \"acme-viewer\",\n      \"name\": \"Acme operations\",\n      \"actorType\": \"user\",\n      \"scopeSlug\": \"acme\",\n      \"appAccess\": [{ \"appSlug\": \"fleet\", \"permission\": \"subscribe\" }]\n    }\n  ],\n  \"signingKeys\": [\n    { \"ref\": \"web\", \"name\": \"Customer web app\" }\n  ]\n}\n","json","",[17,46,47,56,72,81,95,108,128,134,143,170,176,184,190,203,215,228,253,261,274,292,298,306,331,354,360,373,386,416,422,428,436,473,479,485,490,498,503,516,528,540,565,571,576,588,600,612,650,655,660,672,684,696,708,732,737,742,750,774,780],{"__ignoreMap":44},[48,49,52],"span",{"class":50,"line":51},"line",1,[48,53,55],{"class":54},"sVt8B","{\n",[48,57,59,63,66,69],{"class":50,"line":58},2,[48,60,62],{"class":61},"sj4cs","  \"version\"",[48,64,65],{"class":54},": ",[48,67,68],{"class":61},"1",[48,70,71],{"class":54},",\n",[48,73,75,78],{"class":50,"line":74},3,[48,76,77],{"class":61},"  \"project\"",[48,79,80],{"class":54},": {\n",[48,82,84,87,89,93],{"class":50,"line":83},4,[48,85,86],{"class":61},"    \"name\"",[48,88,65],{"class":54},[48,90,92],{"class":91},"sZZnC","\"Fleet\"",[48,94,71],{"class":54},[48,96,98,101,103,106],{"class":50,"line":97},5,[48,99,100],{"class":61},"    \"description\"",[48,102,65],{"class":54},[48,104,105],{"class":91},"\"Vehicles, dispatchers and a customer-facing web app.\"",[48,107,71],{"class":54},[48,109,111,114,117,120,122,125],{"class":50,"line":110},6,[48,112,113],{"class":61},"    \"limits\"",[48,115,116],{"class":54},": { ",[48,118,119],{"class":61},"\"maxConnections\"",[48,121,65],{"class":54},[48,123,124],{"class":61},"500",[48,126,127],{"class":54}," }\n",[48,129,131],{"class":50,"line":130},7,[48,132,133],{"class":54},"  },\n",[48,135,137,140],{"class":50,"line":136},8,[48,138,139],{"class":61},"  \"accessScopes\"",[48,141,142],{"class":54},": [\n",[48,144,146,149,152,154,157,160,163,165,168],{"class":50,"line":145},9,[48,147,148],{"class":54},"    { ",[48,150,151],{"class":61},"\"slug\"",[48,153,65],{"class":54},[48,155,156],{"class":91},"\"acme\"",[48,158,159],{"class":54},", ",[48,161,162],{"class":61},"\"name\"",[48,164,65],{"class":54},[48,166,167],{"class":91},"\"Acme Logistics\"",[48,169,127],{"class":54},[48,171,173],{"class":50,"line":172},10,[48,174,175],{"class":54},"  ],\n",[48,177,179,182],{"class":50,"line":178},11,[48,180,181],{"class":61},"  \"apps\"",[48,183,142],{"class":54},[48,185,187],{"class":50,"line":186},12,[48,188,189],{"class":54},"    {\n",[48,191,193,196,198,201],{"class":50,"line":192},13,[48,194,195],{"class":61},"      \"slug\"",[48,197,65],{"class":54},[48,199,200],{"class":91},"\"fleet\"",[48,202,71],{"class":54},[48,204,206,209,211,213],{"class":50,"line":205},14,[48,207,208],{"class":61},"      \"name\"",[48,210,65],{"class":54},[48,212,92],{"class":91},[48,214,71],{"class":54},[48,216,218,221,223,226],{"class":50,"line":217},15,[48,219,220],{"class":61},"      \"accessMode\"",[48,222,65],{"class":54},[48,224,225],{"class":91},"\"restricted\"",[48,227,71],{"class":54},[48,229,231,234,237,240,242,245,247,250],{"class":50,"line":230},16,[48,232,233],{"class":61},"      \"topics\"",[48,235,236],{"class":54},": [",[48,238,239],{"class":91},"\"location\"",[48,241,159],{"class":54},[48,243,244],{"class":91},"\"status\"",[48,246,159],{"class":54},[48,248,249],{"class":91},"\"commands\"",[48,251,252],{"class":54},"],\n",[48,254,256,259],{"class":50,"line":255},17,[48,257,258],{"class":61},"      \"triggerWebhook\"",[48,260,80],{"class":54},[48,262,264,267,269,272],{"class":50,"line":263},18,[48,265,266],{"class":61},"        \"url\"",[48,268,65],{"class":54},[48,270,271],{"class":91},"\"https://hooks.example.com/nolag/fleet\"",[48,273,71],{"class":54},[48,275,277,280,282,285,287,290],{"class":50,"line":276},19,[48,278,279],{"class":61},"        \"headers\"",[48,281,116],{"class":54},[48,283,284],{"class":61},"\"Authorization\"",[48,286,65],{"class":54},[48,288,289],{"class":91},"\"Bearer replace-me\"",[48,291,127],{"class":54},[48,293,295],{"class":50,"line":294},20,[48,296,297],{"class":54},"      },\n",[48,299,301,304],{"class":50,"line":300},21,[48,302,303],{"class":61},"      \"rooms\"",[48,305,142],{"class":54},[48,307,309,312,314,316,319,321,323,325,328],{"class":50,"line":308},22,[48,310,311],{"class":54},"        { ",[48,313,151],{"class":61},[48,315,65],{"class":54},[48,317,318],{"class":91},"\"north\"",[48,320,159],{"class":54},[48,322,162],{"class":61},[48,324,65],{"class":54},[48,326,327],{"class":91},"\"North depot\"",[48,329,330],{"class":54}," },\n",[48,332,334,336,338,340,343,345,347,349,352],{"class":50,"line":333},23,[48,335,311],{"class":54},[48,337,151],{"class":61},[48,339,65],{"class":54},[48,341,342],{"class":91},"\"south\"",[48,344,159],{"class":54},[48,346,162],{"class":61},[48,348,65],{"class":54},[48,350,351],{"class":91},"\"South depot\"",[48,353,330],{"class":54},[48,355,357],{"class":50,"line":356},24,[48,358,359],{"class":54},"        {\n",[48,361,363,366,368,371],{"class":50,"line":362},25,[48,364,365],{"class":61},"          \"slug\"",[48,367,65],{"class":54},[48,369,370],{"class":91},"\"dispatch\"",[48,372,71],{"class":54},[48,374,376,379,381,384],{"class":50,"line":375},26,[48,377,378],{"class":61},"          \"name\"",[48,380,65],{"class":54},[48,382,383],{"class":91},"\"Dispatch\"",[48,385,71],{"class":54},[48,387,389,392,395,398,400,403,405,408,410,413],{"class":50,"line":388},27,[48,390,391],{"class":61},"          \"typeGrants\"",[48,393,394],{"class":54},": [{ ",[48,396,397],{"class":61},"\"actorType\"",[48,399,65],{"class":54},[48,401,402],{"class":91},"\"service\"",[48,404,159],{"class":54},[48,406,407],{"class":61},"\"permission\"",[48,409,65],{"class":54},[48,411,412],{"class":91},"\"pubSub\"",[48,414,415],{"class":54}," }]\n",[48,417,419],{"class":50,"line":418},28,[48,420,421],{"class":54},"        }\n",[48,423,425],{"class":50,"line":424},29,[48,426,427],{"class":54},"      ],\n",[48,429,431,434],{"class":50,"line":430},30,[48,432,433],{"class":61},"      \"lobbies\"",[48,435,142],{"class":54},[48,437,439,441,443,445,448,450,452,454,457,459,462,464,466,468,470],{"class":50,"line":438},31,[48,440,311],{"class":54},[48,442,151],{"class":61},[48,444,65],{"class":54},[48,446,447],{"class":91},"\"all-depots\"",[48,449,159],{"class":54},[48,451,162],{"class":61},[48,453,65],{"class":54},[48,455,456],{"class":91},"\"All depots\"",[48,458,159],{"class":54},[48,460,461],{"class":61},"\"rooms\"",[48,463,236],{"class":54},[48,465,318],{"class":91},[48,467,159],{"class":54},[48,469,342],{"class":91},[48,471,472],{"class":54},"] }\n",[48,474,476],{"class":50,"line":475},32,[48,477,478],{"class":54},"      ]\n",[48,480,482],{"class":50,"line":481},33,[48,483,484],{"class":54},"    }\n",[48,486,488],{"class":50,"line":487},34,[48,489,175],{"class":54},[48,491,493,496],{"class":50,"line":492},35,[48,494,495],{"class":61},"  \"actors\"",[48,497,142],{"class":54},[48,499,501],{"class":50,"line":500},36,[48,502,189],{"class":54},[48,504,506,509,511,514],{"class":50,"line":505},37,[48,507,508],{"class":61},"      \"ref\"",[48,510,65],{"class":54},[48,512,513],{"class":91},"\"dispatcher\"",[48,515,71],{"class":54},[48,517,519,521,523,526],{"class":50,"line":518},38,[48,520,208],{"class":61},[48,522,65],{"class":54},[48,524,525],{"class":91},"\"Dispatch service\"",[48,527,71],{"class":54},[48,529,531,534,536,538],{"class":50,"line":530},39,[48,532,533],{"class":61},"      \"actorType\"",[48,535,65],{"class":54},[48,537,402],{"class":91},[48,539,71],{"class":54},[48,541,543,546,548,551,553,555,557,559,561,563],{"class":50,"line":542},40,[48,544,545],{"class":61},"      \"appAccess\"",[48,547,394],{"class":54},[48,549,550],{"class":61},"\"appSlug\"",[48,552,65],{"class":54},[48,554,200],{"class":91},[48,556,159],{"class":54},[48,558,407],{"class":61},[48,560,65],{"class":54},[48,562,412],{"class":91},[48,564,415],{"class":54},[48,566,568],{"class":50,"line":567},41,[48,569,570],{"class":54},"    },\n",[48,572,574],{"class":50,"line":573},42,[48,575,189],{"class":54},[48,577,579,581,583,586],{"class":50,"line":578},43,[48,580,508],{"class":61},[48,582,65],{"class":54},[48,584,585],{"class":91},"\"truck-17\"",[48,587,71],{"class":54},[48,589,591,593,595,598],{"class":50,"line":590},44,[48,592,208],{"class":61},[48,594,65],{"class":54},[48,596,597],{"class":91},"\"Truck 17\"",[48,599,71],{"class":54},[48,601,603,605,607,610],{"class":50,"line":602},45,[48,604,533],{"class":61},[48,606,65],{"class":54},[48,608,609],{"class":91},"\"device\"",[48,611,71],{"class":54},[48,613,615,617,619,621,623,625,627,629,631,634,636,639,641,643,645,647],{"class":50,"line":614},46,[48,616,545],{"class":61},[48,618,394],{"class":54},[48,620,550],{"class":61},[48,622,65],{"class":54},[48,624,200],{"class":91},[48,626,159],{"class":54},[48,628,407],{"class":61},[48,630,65],{"class":54},[48,632,633],{"class":91},"\"publish\"",[48,635,159],{"class":54},[48,637,638],{"class":61},"\"topics\"",[48,640,236],{"class":54},[48,642,239],{"class":91},[48,644,159],{"class":54},[48,646,244],{"class":91},[48,648,649],{"class":54},"] }]\n",[48,651,653],{"class":50,"line":652},47,[48,654,570],{"class":54},[48,656,658],{"class":50,"line":657},48,[48,659,189],{"class":54},[48,661,663,665,667,670],{"class":50,"line":662},49,[48,664,508],{"class":61},[48,666,65],{"class":54},[48,668,669],{"class":91},"\"acme-viewer\"",[48,671,71],{"class":54},[48,673,675,677,679,682],{"class":50,"line":674},50,[48,676,208],{"class":61},[48,678,65],{"class":54},[48,680,681],{"class":91},"\"Acme operations\"",[48,683,71],{"class":54},[48,685,687,689,691,694],{"class":50,"line":686},51,[48,688,533],{"class":61},[48,690,65],{"class":54},[48,692,693],{"class":91},"\"user\"",[48,695,71],{"class":54},[48,697,699,702,704,706],{"class":50,"line":698},52,[48,700,701],{"class":61},"      \"scopeSlug\"",[48,703,65],{"class":54},[48,705,156],{"class":91},[48,707,71],{"class":54},[48,709,711,713,715,717,719,721,723,725,727,730],{"class":50,"line":710},53,[48,712,545],{"class":61},[48,714,394],{"class":54},[48,716,550],{"class":61},[48,718,65],{"class":54},[48,720,200],{"class":91},[48,722,159],{"class":54},[48,724,407],{"class":61},[48,726,65],{"class":54},[48,728,729],{"class":91},"\"subscribe\"",[48,731,415],{"class":54},[48,733,735],{"class":50,"line":734},54,[48,736,484],{"class":54},[48,738,740],{"class":50,"line":739},55,[48,741,175],{"class":54},[48,743,745,748],{"class":50,"line":744},56,[48,746,747],{"class":61},"  \"signingKeys\"",[48,749,142],{"class":54},[48,751,753,755,758,760,763,765,767,769,772],{"class":50,"line":752},57,[48,754,148],{"class":54},[48,756,757],{"class":61},"\"ref\"",[48,759,65],{"class":54},[48,761,762],{"class":91},"\"web\"",[48,764,159],{"class":54},[48,766,162],{"class":61},[48,768,65],{"class":54},[48,770,771],{"class":91},"\"Customer web app\"",[48,773,127],{"class":54},[48,775,777],{"class":50,"line":776},58,[48,778,779],{"class":54},"  ]\n",[48,781,783],{"class":50,"line":782},59,[48,784,785],{"class":54},"}\n",[14,787,788],{},"We imported this document into core v0.5.0 and connected each actor. What each one may do:",[790,791,792,819,844],"ul",{},[793,794,795,798,799,802,803,806,807,810,811,814,815,818],"li",{},[17,796,797],{},"dispatcher"," reaches every room of ",[17,800,801],{},"fleet",": the app grant covers the public rooms ",[17,804,805],{},"north"," and ",[17,808,809],{},"south",", and the ",[17,812,813],{},"dispatch"," room's type grant admits ",[17,816,817],{},"service"," actors.",[793,820,821,824,825,806,828,831,832,806,834,836,837,840,841,843],{},[17,822,823],{},"truck-17"," may publish ",[17,826,827],{},"location",[17,829,830],{},"status"," in ",[17,833,805],{},[17,835,809],{},", and nothing else. It cannot subscribe, cannot publish ",[17,838,839],{},"commands",", and cannot reach the private ",[17,842,813],{}," room.",[793,845,846,849,850,853,854,857],{},[17,847,848],{},"acme-viewer"," belongs to the ",[17,851,852],{},"acme"," scope, so its topics live under ",[17,855,856],{},"fleet/acme/\u003Croom>/\u003Ctopic>",", apart from everyone else's. It sees only what actors in the same scope publish.",[34,859,861],{"id":860},"importing-and-exporting","Importing and exporting",[14,863,864],{},"With core's example host:",[39,866,870],{"className":867,"code":868,"language":869,"meta":44,"style":44},"language-bash shiki shiki-themes github-light github-dark","curl -X POST http://localhost:3400/v1/projects/import \\\n  -H 'Content-Type: application/json' \\\n  --data-binary @project.json\n","bash",[17,871,872,890,900],{"__ignoreMap":44},[48,873,874,878,881,884,887],{"class":50,"line":51},[48,875,877],{"class":876},"sScJk","curl",[48,879,880],{"class":61}," -X",[48,882,883],{"class":91}," POST",[48,885,886],{"class":91}," http://localhost:3400/v1/projects/import",[48,888,889],{"class":61}," \\\n",[48,891,892,895,898],{"class":50,"line":58},[48,893,894],{"class":61},"  -H",[48,896,897],{"class":91}," 'Content-Type: application/json'",[48,899,889],{"class":61},[48,901,902,905],{"class":50,"line":74},[48,903,904],{"class":61},"  --data-binary",[48,906,907],{"class":91}," @project.json\n",[14,909,910,911,914,915,918],{},"A successful import returns ",[17,912,913],{},"201"," with every credential it minted, keyed by the ",[17,916,917],{},"ref"," you gave each actor and signing key:",[39,920,922],{"className":41,"code":921,"language":43,"meta":44,"style":44},"{\n  \"projectId\": \"01a11efb-7c17-72da-81c3-4d4c15e4c609\",\n  \"actors\": [\n    { \"ref\": \"dispatcher\", \"keyId\": \"at_live_69a42ea6117c\", \"accessToken\": \"at_live_69a42ea6117c.\u003Csecret>\" }\n  ],\n  \"signingKeys\": [\n    { \"ref\": \"web\", \"keyId\": \"sk_live_34fc93326d35\", \"signingKey\": \"sk_live_34fc93326d35.\u003Csecret>\" }\n  ]\n}\n",[17,923,924,928,940,946,978,982,988,1019,1023],{"__ignoreMap":44},[48,925,926],{"class":50,"line":51},[48,927,55],{"class":54},[48,929,930,933,935,938],{"class":50,"line":58},[48,931,932],{"class":61},"  \"projectId\"",[48,934,65],{"class":54},[48,936,937],{"class":91},"\"01a11efb-7c17-72da-81c3-4d4c15e4c609\"",[48,939,71],{"class":54},[48,941,942,944],{"class":50,"line":74},[48,943,495],{"class":61},[48,945,142],{"class":54},[48,947,948,950,952,954,956,958,961,963,966,968,971,973,976],{"class":50,"line":83},[48,949,148],{"class":54},[48,951,757],{"class":61},[48,953,65],{"class":54},[48,955,513],{"class":91},[48,957,159],{"class":54},[48,959,960],{"class":61},"\"keyId\"",[48,962,65],{"class":54},[48,964,965],{"class":91},"\"at_live_69a42ea6117c\"",[48,967,159],{"class":54},[48,969,970],{"class":61},"\"accessToken\"",[48,972,65],{"class":54},[48,974,975],{"class":91},"\"at_live_69a42ea6117c.\u003Csecret>\"",[48,977,127],{"class":54},[48,979,980],{"class":50,"line":97},[48,981,175],{"class":54},[48,983,984,986],{"class":50,"line":110},[48,985,747],{"class":61},[48,987,142],{"class":54},[48,989,990,992,994,996,998,1000,1002,1004,1007,1009,1012,1014,1017],{"class":50,"line":130},[48,991,148],{"class":54},[48,993,757],{"class":61},[48,995,65],{"class":54},[48,997,762],{"class":91},[48,999,159],{"class":54},[48,1001,960],{"class":61},[48,1003,65],{"class":54},[48,1005,1006],{"class":91},"\"sk_live_34fc93326d35\"",[48,1008,159],{"class":54},[48,1010,1011],{"class":61},"\"signingKey\"",[48,1013,65],{"class":54},[48,1015,1016],{"class":91},"\"sk_live_34fc93326d35.\u003Csecret>\"",[48,1018,127],{"class":54},[48,1020,1021],{"class":50,"line":136},[48,1022,779],{"class":54},[48,1024,1025],{"class":50,"line":145},[48,1026,785],{"class":54},[14,1028,1029],{},"Store these straight away: core keeps only a hash of each actor secret and an encrypted copy of each signing key secret, and shows neither again.",[14,1031,1032],{},"To export:",[39,1034,1036],{"className":867,"code":1035,"language":869,"meta":44,"style":44},"curl http://localhost:3400/v1/projects/01a11efb-7c17-72da-81c3-4d4c15e4c609/export\n",[17,1037,1038],{"__ignoreMap":44},[48,1039,1040,1042],{"class":50,"line":51},[48,1041,877],{"class":876},[48,1043,1044],{"class":91}," http://localhost:3400/v1/projects/01a11efb-7c17-72da-81c3-4d4c15e4c609/export\n",[14,1046,1047,1048,1050],{},"The export has the same shape as the import document. Each actor's and signing key's ",[17,1049,917],{}," becomes its key id, and no secret appears anywhere.",[14,1052,1053,1054,1057,1058,1061,1062,1064,1065,1067],{},"The import runs in a single transaction, so a document that fails part-way writes nothing. It is refused with ",[17,1055,1056],{},"400"," if ",[17,1059,1060],{},"version"," is not ",[17,1063,68],{},", if a slug breaks the slug rule, if a property is not part of the format (the example host rejects unknown properties rather than ignoring them), if two access scopes, apps, actors or signing keys share a slug or ",[17,1066,917],{},", or if anything refers to a scope, app or room the document does not define.",[34,1069,1071],{"id":1070},"field-reference","Field reference",[14,1073,1074,1077],{},[25,1075,1076],{},"Slugs"," (projects aside, everything has one) are 1 to 100 characters of lowercase letters, digits and hyphens, starting and ending with a letter or digit. They appear in topic addresses, which is why the set is narrow.",[14,1079,1080,1083,1084,159,1087,806,1090,1093,1094,1097],{},[25,1081,1082],{},"Topic names"," go in the last segment of an address. Keep them to letters, digits, ",[17,1085,1086],{},"_",[17,1088,1089],{},"-",[17,1091,1092],{},":",", with no ",[17,1095,1096],{},"/",": a slash would change the shape of every address the topic appears in. (Core enforces this rule on its app and room facades; the import format accepts any string, so the rule is yours to keep.)",[1099,1100,1102],"h3",{"id":1101},"top-level","Top level",[1104,1105,1106,1122],"table",{},[1107,1108,1109],"thead",{},[1110,1111,1112,1116,1119],"tr",{},[1113,1114,1115],"th",{},"Field",[1113,1117,1118],{},"Required",[1113,1120,1121],{},"Meaning",[1123,1124,1125,1140,1152,1169,1181,1193],"tbody",{},[1110,1126,1127,1132,1135],{},[1128,1129,1130],"td",{},[17,1131,1060],{},[1128,1133,1134],{},"yes",[1128,1136,1137,1138],{},"Always ",[17,1139,68],{},[1110,1141,1142,1147,1149],{},[1128,1143,1144],{},[17,1145,1146],{},"project",[1128,1148,1134],{},[1128,1150,1151],{},"The project itself",[1110,1153,1154,1159,1161],{},[1128,1155,1156],{},[17,1157,1158],{},"accessScopes",[1128,1160],{},[1128,1162,1163,1164],{},"Tenants within the project. See ",[1165,1166,1168],"a",{"href":1167},"/docs/scopes","Access Scopes",[1110,1170,1171,1176,1178],{},[1128,1172,1173],{},[17,1174,1175],{},"apps",[1128,1177],{},[1128,1179,1180],{},"Apps, each with its rooms and lobbies",[1110,1182,1183,1188,1190],{},[1128,1184,1185],{},[17,1186,1187],{},"actors",[1128,1189],{},[1128,1191,1192],{},"Identities that connect, each with its grants",[1110,1194,1195,1200,1202],{},[1128,1196,1197],{},[17,1198,1199],{},"signingKeys",[1128,1201],{},[1128,1203,1204,1205],{},"Keys for signing ",[1165,1206,1208],{"href":1207},"/docs/client-tokens","client tokens",[1099,1210,1211],{"id":1146},[17,1212,1146],{},[1104,1214,1215,1223],{},[1107,1216,1217],{},[1110,1218,1219,1221],{},[1113,1220,1115],{},[1113,1222,1121],{},[1123,1224,1225,1234,1244,1258,1275,1289],{},[1110,1226,1227,1232],{},[1128,1228,1229],{},[17,1230,1231],{},"name",[1128,1233,1118],{},[1110,1235,1236,1241],{},[1128,1237,1238],{},[17,1239,1240],{},"description",[1128,1242,1243],{},"Free text",[1110,1245,1246,1251],{},[1128,1247,1248],{},[17,1249,1250],{},"organizationId",[1128,1252,1253,1254,1257],{},"Optional. Must be a UUID: any other string makes the import fail. Core stores it and never interprets it, but kraken counts connections for ",[17,1255,1256],{},"limits.maxConnections"," per organization id (see below).",[1110,1259,1260,1264],{},[1128,1261,1262],{},[17,1263,1256],{},[1128,1265,1266,1267,1270,1271,1274],{},"Connection limit, enforced by kraken: a connect beyond it is refused with ",[17,1268,1269],{},"connection_limit_reached",". ",[17,1272,1273],{},"null"," means unlimited.",[1110,1276,1277,1282],{},[1128,1278,1279],{},[17,1280,1281],{},"limits.maxMessageSizeBytes",[1128,1283,1284,1285,1288],{},"Passed to kraken, which in v0.9.0 does ",[25,1286,1287],{},"not"," enforce it. Every publish is held to kraken's fixed 921,600 byte ceiling instead.",[1110,1290,1291,1296],{},[1128,1292,1293],{},[17,1294,1295],{},"limits.sessionExpirySeconds",[1128,1297,1298,1299,806,1302,1305],{},"Session lifetime for ",[17,1300,1301],{},"agent",[17,1303,1304],{},"orchestrator"," actors, used only with kraken's MQTT broker backend",[14,1307,1308,1309,1312,1313,1315,1316,1318],{},"kraken counts connections per ",[25,1310,1311],{},"organization id",", not per project, across every project that shares the id. Projects that leave ",[17,1314,1250],{}," unset all share a single count: with core v0.5.0 and kraken v0.9.0, one connection to a project limited to one connection also refused the first connection to a second project without an organization id. Give every project that has a connection limit its own ",[17,1317,1250],{},".",[14,1320,1321,1322,1325,1326,159,1329,806,1332,1335],{},"A document without ",[17,1323,1324],{},"limits"," leaves the project on the host's defaults (the example host reads ",[17,1327,1328],{},"DEFAULT_MAX_CONNECTIONS",[17,1330,1331],{},"DEFAULT_MAX_MESSAGE_SIZE_BYTES",[17,1333,1334],{},"DEFAULT_SESSION_EXPIRY_SECONDS",", which default to unlimited, unlimited and 3600).",[1099,1337,1339],{"id":1338},"accessscopes",[17,1340,1341],{},"accessScopes[]",[1104,1343,1344,1352],{},[1107,1345,1346],{},[1110,1347,1348,1350],{},[1113,1349,1115],{},[1113,1351,1121],{},[1123,1353,1354,1365,1377],{},[1110,1355,1356,1363],{},[1128,1357,1358,159,1361],{},[17,1359,1360],{},"slug",[17,1362,1231],{},[1128,1364,1118],{},[1110,1366,1367,1374],{},[1128,1368,1369,159,1371],{},[17,1370,1240],{},[17,1372,1373],{},"metadata",[1128,1375,1376],{},"Optional",[1110,1378,1379,1384],{},[1128,1380,1381],{},[17,1382,1383],{},"isActive",[1128,1385,1386,1387,1390,1391,1318],{},"Default ",[17,1388,1389],{},"true",". Actors bound to an inactive scope are refused, and live ones are disconnected at their next revalidation with reason ",[17,1392,1393],{},"scope_inactive",[1099,1395,1396],{"id":1175},[17,1397,1398],{},"apps[]",[1104,1400,1401,1409],{},[1107,1402,1403],{},[1110,1404,1405,1407],{},[1113,1406,1115],{},[1113,1408,1121],{},[1123,1410,1411,1422,1435,1456,1476,1484,1504,1518],{},[1110,1412,1413,1419],{},[1128,1414,1415,159,1417],{},[17,1416,1360],{},[17,1418,1231],{},[1128,1420,1421],{},"Required. The slug is the first segment of every topic address in the app, and is used exactly as written.",[1110,1423,1424,1429],{},[1128,1425,1426],{},[17,1427,1428],{},"topics",[1128,1430,1431,1432,1434],{},"Required. The app's topic list. This is the authoritative list for authorization: a room's own ",[17,1433,1428],{}," are not read.",[1110,1436,1437,1442],{},[1128,1438,1439],{},[17,1440,1441],{},"accessMode",[1128,1443,1444,1447,1448,1451,1452,1455],{},[17,1445,1446],{},"open"," (the default): every active actor in the project reaches the app without a stored grant. ",[17,1449,1450],{},"restricted",": an actor needs an ",[17,1453,1454],{},"appAccess"," grant.",[1110,1457,1458,1462],{},[1128,1459,1460],{},[17,1461,830],{},[1128,1463,1464,1467,1468,1471,1472,1475],{},[17,1465,1466],{},"active"," (the default), ",[17,1469,1470],{},"disabled"," or ",[17,1473,1474],{},"suspended",". Only active apps are reachable.",[1110,1477,1478,1482],{},[1128,1479,1480],{},[17,1481,1240],{},[1128,1483,1376],{},[1110,1485,1486,1494],{},[1128,1487,1488,159,1491],{},[17,1489,1490],{},"hydrationWebhook",[17,1492,1493],{},"triggerWebhook",[1128,1495,1496,1499,1500,1318],{},[17,1497,1498],{},"{ \"url\", \"headers\" }",". Called when an actor subscribes (hydration) or publishes (trigger). See ",[1165,1501,1503],{"href":1502},"/docs/concepts/webhooks","Webhooks",[1110,1505,1506,1511],{},[1128,1507,1508],{},[17,1509,1510],{},"topicConfigs",[1128,1512,1513,1514,1517],{},"Per-topic settings. Per-topic webhooks go here, as ",[17,1515,1516],{},"{ \"\u003Ctopic>\": { \"webhooks\": { \"onSubscribe\": { \"url\", \"headers\" }, \"onPublish\": { \"url\", \"headers\" } } } }",", and take precedence over the app-level ones.",[1110,1519,1520,1528],{},[1128,1521,1522,159,1525],{},[17,1523,1524],{},"rooms",[17,1526,1527],{},"lobbies",[1128,1529,1530],{},"See below",[1099,1532,1534],{"id":1533},"appsrooms",[17,1535,1536],{},"apps[].rooms[]",[1104,1538,1539,1547],{},[1107,1540,1541],{},[1110,1542,1543,1545],{},[1113,1544,1115],{},[1113,1546,1121],{},[1123,1548,1549,1559,1573,1592,1602],{},[1110,1550,1551,1557],{},[1128,1552,1553,159,1555],{},[17,1554,1360],{},[17,1556,1231],{},[1128,1558,1118],{},[1110,1560,1561,1565],{},[1128,1562,1563],{},[17,1564,830],{},[1128,1566,1567,1569,1570,1572],{},[17,1568,1466],{}," (the default) or ",[17,1571,1470],{},". Only active rooms are reachable.",[1110,1574,1575,1580],{},[1128,1576,1577],{},[17,1578,1579],{},"typeGrants",[1128,1581,1582,1583,1270,1586,1588,1589,1591],{},"Grants for every actor of a type: ",[17,1584,1585],{},"{ \"actorType\", \"permission\", \"topics\", \"isActive\" }",[17,1587,1428],{}," of ",[17,1590,1273],{}," (or absent) inherits the app's list.",[1110,1593,1594,1600],{},[1128,1595,1596,159,1598],{},[17,1597,1240],{},[17,1599,1373],{},[1128,1601,1376],{},[1110,1603,1604,1608],{},[1128,1605,1606],{},[17,1607,1428],{},[1128,1609,1610],{},"Stored, but not read during authorization",[14,1612,1613,1616,1617,1619,1620,1623,1624,1626,1627,1630],{},[25,1614,1615],{},"A room is public until something grants access to it."," As soon as any grant names the room, whether a ",[17,1618,1579],{}," entry on the room or a ",[17,1621,1622],{},"roomAccess"," entry on an actor, the room becomes private, and only actors holding a grant for it get in. That includes actors whose app grant would otherwise cover it. We confirmed this with core v0.5.0: one actor's ",[17,1625,1622],{}," on a room shut out another actor that had a full ",[17,1628,1629],{},"pubSub"," grant on the app.",[1099,1632,1634],{"id":1633},"appslobbies",[17,1635,1636],{},"apps[].lobbies[]",[1104,1638,1639,1647],{},[1107,1640,1641],{},[1110,1642,1643,1645],{},[1113,1644,1115],{},[1113,1646,1121],{},[1123,1648,1649,1659,1668],{},[1110,1650,1651,1657],{},[1128,1652,1653,159,1655],{},[17,1654,1360],{},[17,1656,1231],{},[1128,1658,1118],{},[1110,1660,1661,1665],{},[1128,1662,1663],{},[17,1664,1524],{},[1128,1666,1667],{},"Required. Slugs of rooms in the same app.",[1110,1669,1670,1676],{},[1128,1671,1672,159,1674],{},[17,1673,1240],{},[17,1675,1373],{},[1128,1677,1376],{},[14,1679,1680,1681,1318],{},"See ",[1165,1682,1684],{"href":1683},"/docs/concepts/lobbies","Lobbies",[1099,1686,1687],{"id":1187},[17,1688,1689],{},"actors[]",[1104,1691,1692,1700],{},[1107,1693,1694],{},[1110,1695,1696,1698],{},[1113,1697,1115],{},[1113,1699,1121],{},[1123,1701,1702,1711,1746,1758,1768,1778,1798,1819],{},[1110,1703,1704,1708],{},[1128,1705,1706],{},[17,1707,917],{},[1128,1709,1710],{},"Required. A handle for this actor within the document, used only to label its minted token in the import response. Not stored.",[1110,1712,1713,1720],{},[1128,1714,1715,159,1717],{},[17,1716,1231],{},[17,1718,1719],{},"actorType",[1128,1721,1722,1723,159,1726,159,1729,159,1731,159,1734,159,1736,159,1738,1741,1742,1318],{},"Required. Types: ",[17,1724,1725],{},"device",[17,1727,1728],{},"user",[17,1730,817],{},[17,1732,1733],{},"session",[17,1735,1301],{},[17,1737,1304],{},[17,1739,1740],{},"observer",". See ",[1165,1743,1745],{"href":1744},"/docs/authentication#actor-types","Authentication",[1110,1747,1748,1752],{},[1128,1749,1750],{},[17,1751,830],{},[1128,1753,1754,1569,1756],{},[17,1755,1466],{},[17,1757,1470],{},[1110,1759,1760,1765],{},[1128,1761,1762],{},[17,1763,1764],{},"expiresAt",[1128,1766,1767],{},"Optional ISO 8601 time after which the actor is refused",[1110,1769,1770,1775],{},[1128,1771,1772],{},[17,1773,1774],{},"scopeSlug",[1128,1776,1777],{},"Binds the actor to an access scope",[1110,1779,1780,1784],{},[1128,1781,1782],{},[17,1783,1454],{},[1128,1785,1786,1787,1790,1791,1793,1794,1588,1796,1591],{},"App grants: ",[17,1788,1789],{},"{ \"appSlug\", \"permission\", \"topics\", \"isActive\", \"expiresAt\" }",". Required for every app in ",[17,1792,1450],{}," mode. ",[17,1795,1428],{},[17,1797,1273],{},[1110,1799,1800,1804],{},[1128,1801,1802],{},[17,1803,1622],{},[1128,1805,1806,1807,1270,1810,1813,1814,1816,1817,1455],{},"Room grants: ",[17,1808,1809],{},"{ \"appSlug\", \"roomSlug\", \"permission\", \"topics\", \"isActive\", \"expiresAt\", \"role\" }",[17,1811,1812],{},"role"," is a display label only. In a ",[17,1815,1450],{}," app the actor also needs an ",[17,1818,1454],{},[1110,1820,1821,1825],{},[1128,1822,1823],{},[17,1824,1373],{},[1128,1826,1376],{},[14,1828,1829,1832,1833,159,1836,1471,1839,1841,1842,1844,1845,1847],{},[17,1830,1831],{},"permission"," is ",[17,1834,1835],{},"subscribe",[17,1837,1838],{},"publish",[17,1840,1629],{}," everywhere. A grant past its ",[17,1843,1764],{},", or with ",[17,1846,1383],{}," false, is ignored. When an actor holds both kinds, a grant naming the actor takes precedence over a type grant on the room.",[1099,1849,1851],{"id":1850},"signingkeys",[17,1852,1853],{},"signingKeys[]",[1104,1855,1856,1864],{},[1107,1857,1858],{},[1110,1859,1860,1862],{},[1113,1861,1115],{},[1113,1863,1121],{},[1123,1865,1866,1875],{},[1110,1867,1868,1872],{},[1128,1869,1870],{},[17,1871,917],{},[1128,1873,1874],{},"Required. Labels the minted key in the import response.",[1110,1876,1877,1881],{},[1128,1878,1879],{},[17,1880,1231],{},[1128,1882,1118],{},[14,1884,1885,1886,1318],{},"Importing signing keys needs the host to have a signing key encryption key configured. See ",[1165,1887,1888],{"href":1207},"Client Tokens",[34,1890,1892],{"id":1891},"the-example-hosts-routes","The example host's routes",[14,1894,1895,1896,1899,1900,1903],{},"The example host in the nolag-core repository serves the import and export routes above and the endpoints kraken calls, and nothing else. ",[25,1897,1898],{},"None of them asks for any credential",", and the example host does not check kraken's ",[17,1901,1902],{},"BACKEND_SECRET"," either. Keep it on localhost, or put it behind your own authentication.",[1104,1905,1906,1916],{},[1107,1907,1908],{},[1110,1909,1910,1913],{},[1113,1911,1912],{},"Route",[1113,1914,1915],{},"What it does",[1123,1917,1918,1935,1945,1963,1979,1993,2006,2019,2031,2043],{},[1110,1919,1920,1925],{},[1128,1921,1922],{},[17,1923,1924],{},"GET /health",[1128,1926,1927,1930,1931,1934],{},[17,1928,1929],{},"{\"status\":\"ok\",\"database\":\"up\"}",", or ",[17,1932,1933],{},"degraded"," when Postgres is unreachable",[1110,1936,1937,1942],{},[1128,1938,1939],{},[17,1940,1941],{},"GET /swagger",[1128,1943,1944],{},"OpenAPI documentation for every route below",[1110,1946,1947,1952],{},[1128,1948,1949],{},[17,1950,1951],{},"GET /v1/projects",[1128,1953,1954,1955,159,1958,159,1960],{},"Lists projects: ",[17,1956,1957],{},"projectId",[17,1959,1231],{},[17,1961,1962],{},"createdAt",[1110,1964,1965,1970],{},[1128,1966,1967],{},[17,1968,1969],{},"POST /v1/projects/import",[1128,1971,1972,1973,1975,1976,1978],{},"Creates a project from a document. ",[17,1974,913],{}," with the minted credentials, ",[17,1977,1056],{}," for an invalid document.",[1110,1980,1981,1986],{},[1128,1982,1983],{},[17,1984,1985],{},"GET /v1/projects/{projectId}/export",[1128,1987,1988,1989,1992],{},"The project as a document, without secrets. ",[17,1990,1991],{},"404"," for an unknown project.",[1110,1994,1995,2000],{},[1128,1996,1997],{},[17,1998,1999],{},"DELETE /v1/projects/{projectId}",[1128,2001,2002,2003,1318],{},"Deletes the project and everything in it. ",[17,2004,2005],{},"204",[1110,2007,2008,2013],{},[1128,2009,2010],{},[17,2011,2012],{},"POST /v1/internal/actors/validate",[1128,2014,2015,2016],{},"kraken's ",[17,2017,2018],{},"/validate",[1110,2020,2021,2026],{},[1128,2022,2023],{},[17,2024,2025],{},"POST /v1/internal/actors/revalidate",[1128,2027,2015,2028],{},[17,2029,2030],{},"/revalidate",[1110,2032,2033,2038],{},[1128,2034,2035],{},[17,2036,2037],{},"POST /v1/internal/actors/check-room-access",[1128,2039,2015,2040],{},[17,2041,2042],{},"/check-room-access",[1110,2044,2045,2050],{},[1128,2046,2047],{},[17,2048,2049],{},"POST /v1/internal/subscriptions/update",[1128,2051,2052,2053,1270,2056,2058],{},"Records one subscribe or unsubscribe for an actor, ",[17,2054,2055],{},"{ \"actorTokenId\", \"topic\", \"action\", \"loadBalance\", \"loadBalanceGroup\", \"filters\" }",[17,2057,2005],{},". kraken v0.9.0 does not call this route.",[14,2060,2061,2062,2065,2066,2069,2070,2074,2075,2078],{},"So kraken's ",[17,2063,2064],{},"AUTH_HTTP_URL"," for this host is ",[17,2067,2068],{},"http://\u003Chost>:3000/v1/internal/actors",". The three actor routes follow the ",[1165,2071,2073],{"href":2072},"/docs/self-hosting/http-auth","HTTP auth contract",", and always answer ",[17,2076,2077],{},"200",", with the denial in the body.",[14,2080,2081,2082,1318],{},"There is no route for a single app, room, actor or signing key. To change one, export the project, edit the document and import it as a new project (which mints new credentials), or write a host of your own on core's facades. See ",[1165,2083,2085],{"href":2084},"/docs/self-hosting/full-stack#running-core-in-your-own-host","Full Stack",[2087,2088,2089],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":44,"searchDepth":58,"depth":58,"links":2091},[2092,2093,2094,2104],{"id":36,"depth":58,"text":37},{"id":860,"depth":58,"text":861},{"id":1070,"depth":58,"text":1071,"children":2095},[2096,2097,2098,2099,2100,2101,2102,2103],{"id":1101,"depth":74,"text":1102},{"id":1146,"depth":74,"text":1146},{"id":1338,"depth":74,"text":1341},{"id":1175,"depth":74,"text":1398},{"id":1533,"depth":74,"text":1536},{"id":1633,"depth":74,"text":1636},{"id":1187,"depth":74,"text":1689},{"id":1850,"depth":74,"text":1853},{"id":1891,"depth":58,"text":1892},"The JSON document @nolag/core imports to create a whole project (apps, rooms, lobbies, access scopes, actors and signing keys) and exports again without secrets, plus the HTTP routes of core's example host.","md",{},true,"/docs/self-hosting/project-document",{"title":5,"description":2105},"docs/self-hosting/project-document","ZgjcY7B3rYeOUmgByvuvr4TPcTacZqJgrxCYHiqwLxk",1791536075104]