[{"data":1,"prerenderedAt":1722},["ShallowReactive",2],{"docs:/docs/self-hosting/full-stack":3},{"id":4,"title":5,"body":6,"description":1715,"extension":1716,"meta":1717,"navigation":761,"path":1718,"seo":1719,"stem":1720,"__hash__":1721},"docs/docs/self-hosting/full-stack.md","Full Stack with @nolag/core",{"type":7,"value":8,"toc":1703},"minimark",[9,13,30,51,56,69,73,131,136,139,178,181,253,273,276,307,311,316,397,493,504,509,638,641,656,660,666,708,719,1137,1145,1148,1254,1267,1271,1281,1303,1306,1310,1327,1331,1364,1368,1371,1426,1430,1433,1479,1655,1679,1683,1699],[10,11,5],"h1",{"id":12},"full-stack-with-nolagcore",[14,15,16,17,24,25,29],"p",{},"The ",[18,19,23],"a",{"href":20,"rel":21},"https://github.com/NoLagApp/nolag-core",[22],"nofollow","nolag-core"," repository brings up the whole system on your machine with one script: Postgres, an example host that mounts the ",[26,27,28],"code",{},"@nolag/core"," library, kraken pointed at it, and a small admin UI. It then imports a demo project and writes out the credentials it minted.",[31,32,34],"callout",{"type":33},"warning",[14,35,36,37,41,42,45,46,50],{},"The quickstart is a demonstration, not a deployment. The example host ",[38,39,40],"strong",{},"authenticates nobody",": anyone who can reach its port can read, create and delete every project and mint credentials for any of them. That is why every published port binds to ",[26,43,44],{},"127.0.0.1",". Do not put it on a network. See ",[18,47,49],{"href":48},"#what-the-quickstart-is-not","what the quickstart is not",".",[52,53,55],"h2",{"id":54},"prerequisites","Prerequisites",[57,58,59,66],"ul",{},[60,61,62,63],"li",{},"Docker with Docker Compose, Git and ",[26,64,65],{},"curl",[60,67,68],{},"Node.js 18 or later, to connect a client and run the test suite",[52,70,72],{"id":71},"run-it","Run it",[74,75,80],"pre",{"className":76,"code":77,"language":78,"meta":79,"style":79},"language-bash shiki shiki-themes github-light github-dark","git clone --branch v0.5.0 https://github.com/NoLagApp/nolag-core.git\ncd nolag-core\nKRAKEN_CONTEXT=https://github.com/NoLagApp/kraken.git#v0.9.0 ./quickstart/quickstart.sh\n","bash","",[26,81,82,105,114],{"__ignoreMap":79},[83,84,87,91,95,99,102],"span",{"class":85,"line":86},"line",1,[83,88,90],{"class":89},"sScJk","git",[83,92,94],{"class":93},"sZZnC"," clone",[83,96,98],{"class":97},"sj4cs"," --branch",[83,100,101],{"class":93}," v0.5.0",[83,103,104],{"class":93}," https://github.com/NoLagApp/nolag-core.git\n",[83,106,108,111],{"class":85,"line":107},2,[83,109,110],{"class":97},"cd",[83,112,113],{"class":93}," nolag-core\n",[83,115,117,121,125,128],{"class":85,"line":116},3,[83,118,120],{"class":119},"sVt8B","KRAKEN_CONTEXT",[83,122,124],{"class":123},"szBVR","=",[83,126,127],{"class":93},"https://github.com/NoLagApp/kraken.git",[83,129,130],{"class":119},"#v0.9.0 ./quickstart/quickstart.sh\n",[14,132,133,135],{},[26,134,120],{}," tells Docker where to build kraken from. The compose file in nolag-core v0.5.0 points at a kraken branch that no longer exists, so set it to the kraken v0.9.0 release as above, or to the path of a local kraken checkout.",[14,137,138],{},"The script:",[140,141,142,153,160,171],"ol",{},[60,143,144,145,148,149,152],{},"writes a ",[26,146,147],{},".env"," file on its first run, with a generated ",[26,150,151],{},"SIGNING_KEY_ENCRYPTION_KEY",", Postgres password and kraken internal secret, and the published ports;",[60,154,155,156,159],{},"runs ",[26,157,158],{},"docker compose up -d --build --wait",", which builds core, the UI and kraken from source and waits until every service reports healthy (the first build takes a few minutes);",[60,161,162,163,166,167,170],{},"imports ",[26,164,165],{},"quickstart/demo-project.json"," through the host's ",[26,168,169],{},"POST /v1/projects/import",";",[60,172,173,174,177],{},"writes the minted credentials to ",[26,175,176],{},"quickstart/credentials.json",", readable only by you.",[14,179,180],{},"When it finishes:",[182,183,184,197],"table",{},[185,186,187],"thead",{},[188,189,190,194],"tr",{},[191,192,193],"th",{},"Service",[191,195,196],{},"Address",[198,199,200,211,225,235],"tbody",{},[188,201,202,206],{},[203,204,205],"td",{},"Admin UI",[203,207,208],{},[26,209,210],{},"http://localhost:3401",[188,212,213,216],{},[203,214,215],{},"Example host (core's HTTP API)",[203,217,218,221,222],{},[26,219,220],{},"http://localhost:3400",", with OpenAPI docs at ",[26,223,224],{},"/swagger",[188,226,227,230],{},[203,228,229],{},"Broker",[203,231,232],{},[26,233,234],{},"ws://localhost:8410/ws",[188,236,237,240],{},[203,238,239],{},"Postgres",[203,241,242,245,246,249,250],{},[26,243,244],{},"localhost:5442",", user ",[26,247,248],{},"nolag",", database ",[26,251,252],{},"nolag_core",[14,254,255,256,258,259,262,263,262,266,262,269,272],{},"The ports come from ",[26,257,147],{}," (",[26,260,261],{},"UI_PORT",", ",[26,264,265],{},"CORE_PORT",[26,267,268],{},"KRAKEN_PORT",[26,270,271],{},"POSTGRES_PORT","), so change them there if something else holds one. Port 18843 is published for kraken's MQTT listener too, but that listener does not accept connections in kraken v0.9.0.",[14,274,275],{},"Check both halves:",[74,277,279],{"className":76,"code":278,"language":78,"meta":79,"style":79},"curl localhost:3400/health\n# {\"status\":\"ok\",\"database\":\"up\"}\ncurl localhost:8410/health\n# {\"status\":\"ok\"}\n",[26,280,281,288,294,301],{"__ignoreMap":79},[83,282,283,285],{"class":85,"line":86},[83,284,65],{"class":89},[83,286,287],{"class":93}," localhost:3400/health\n",[83,289,290],{"class":85,"line":107},[83,291,293],{"class":292},"sJ8bj","# {\"status\":\"ok\",\"database\":\"up\"}\n",[83,295,296,298],{"class":85,"line":116},[83,297,65],{"class":89},[83,299,300],{"class":93}," localhost:8410/health\n",[83,302,304],{"class":85,"line":303},4,[83,305,306],{"class":292},"# {\"status\":\"ok\"}\n",[52,308,310],{"id":309},"the-demo-project","The demo project",[14,312,313,315],{},[26,314,165],{}," defines:",[182,317,318,334],{},[185,319,320],{},[188,321,322,325,328,331],{},[191,323,324],{},"App",[191,326,327],{},"Access",[191,329,330],{},"Topics",[191,332,333],{},"Rooms",[198,335,336,374],{},[188,337,338,343,349,357],{},[203,339,340],{},[26,341,342],{},"chat",[203,344,345,348],{},[26,346,347],{},"open",": every active actor in the project reaches it without a grant",[203,350,351,262,354],{},[26,352,353],{},"messages",[26,355,356],{},"typing",[203,358,359,262,362,365,366,369,370,373],{},[26,360,361],{},"general",[26,363,364],{},"random",", and ",[26,367,368],{},"vip",", which a type grant makes private to ",[26,371,372],{},"service"," actors",[188,375,376,381,387,392],{},[203,377,378],{},[26,379,380],{},"ops",[203,382,383,386],{},[26,384,385],{},"restricted",": needs an explicit grant on the actor",[203,388,389],{},[26,390,391],{},"alerts",[203,393,394],{},[26,395,396],{},"control",[182,398,399,412],{},[185,400,401],{},[188,402,403,406,409],{},[191,404,405],{},"Actor",[191,407,408],{},"Type",[191,410,411],{},"Notes",[198,413,414,435,459,477],{},[188,415,416,424,429],{},[203,417,418,262,421],{},[26,419,420],{},"alice",[26,422,423],{},"bob",[203,425,426],{},[26,427,428],{},"user",[203,430,431,432,434],{},"No grants of their own: they reach the open ",[26,433,342],{}," app only",[188,436,437,442,446],{},[203,438,439],{},[26,440,441],{},"opsbot",[203,443,444],{},[26,445,372],{},[203,447,448,449,452,453,455,456,458],{},"Granted ",[26,450,451],{},"pubSub"," on the ",[26,454,380],{}," app; reaches the ",[26,457,368],{}," room through the type grant",[188,460,461,466,471],{},[203,462,463],{},[26,464,465],{},"acme-device",[203,467,468],{},[26,469,470],{},"device",[203,472,473,474],{},"Bound to access scope ",[26,475,476],{},"acme",[188,478,479,484,488],{},[203,480,481],{},[26,482,483],{},"globex-device",[203,485,486],{},[26,487,470],{},[203,489,473,490],{},[26,491,492],{},"globex",[14,494,495,496,499,500,50],{},"It also creates one signing key, ",[26,497,498],{},"browser",", for ",[18,501,503],{"href":502},"/docs/client-tokens","client tokens",[14,505,506,508],{},[26,507,176],{}," holds what the import minted:",[74,510,514],{"className":511,"code":512,"language":513,"meta":79,"style":79},"language-json shiki shiki-themes github-light github-dark","{\n  \"projectId\": \"01a11ee7-a399-74bc-b934-161d1cc618a4\",\n  \"actors\": [\n    { \"ref\": \"alice\", \"keyId\": \"at_live_9c6b0cdc3c4f\", \"accessToken\": \"at_live_9c6b0cdc3c4f.\u003Csecret>\" }\n  ],\n  \"signingKeys\": [\n    { \"ref\": \"browser\", \"keyId\": \"sk_live_d4dcb310e8b8\", \"signingKey\": \"sk_live_d4dcb310e8b8.\u003Csecret>\" }\n  ]\n}\n","json",[26,515,516,521,535,543,579,585,593,626,632],{"__ignoreMap":79},[83,517,518],{"class":85,"line":86},[83,519,520],{"class":119},"{\n",[83,522,523,526,529,532],{"class":85,"line":107},[83,524,525],{"class":97},"  \"projectId\"",[83,527,528],{"class":119},": ",[83,530,531],{"class":93},"\"01a11ee7-a399-74bc-b934-161d1cc618a4\"",[83,533,534],{"class":119},",\n",[83,536,537,540],{"class":85,"line":116},[83,538,539],{"class":97},"  \"actors\"",[83,541,542],{"class":119},": [\n",[83,544,545,548,551,553,556,558,561,563,566,568,571,573,576],{"class":85,"line":303},[83,546,547],{"class":119},"    { ",[83,549,550],{"class":97},"\"ref\"",[83,552,528],{"class":119},[83,554,555],{"class":93},"\"alice\"",[83,557,262],{"class":119},[83,559,560],{"class":97},"\"keyId\"",[83,562,528],{"class":119},[83,564,565],{"class":93},"\"at_live_9c6b0cdc3c4f\"",[83,567,262],{"class":119},[83,569,570],{"class":97},"\"accessToken\"",[83,572,528],{"class":119},[83,574,575],{"class":93},"\"at_live_9c6b0cdc3c4f.\u003Csecret>\"",[83,577,578],{"class":119}," }\n",[83,580,582],{"class":85,"line":581},5,[83,583,584],{"class":119},"  ],\n",[83,586,588,591],{"class":85,"line":587},6,[83,589,590],{"class":97},"  \"signingKeys\"",[83,592,542],{"class":119},[83,594,596,598,600,602,605,607,609,611,614,616,619,621,624],{"class":85,"line":595},7,[83,597,547],{"class":119},[83,599,550],{"class":97},[83,601,528],{"class":119},[83,603,604],{"class":93},"\"browser\"",[83,606,262],{"class":119},[83,608,560],{"class":97},[83,610,528],{"class":119},[83,612,613],{"class":93},"\"sk_live_d4dcb310e8b8\"",[83,615,262],{"class":119},[83,617,618],{"class":97},"\"signingKey\"",[83,620,528],{"class":119},[83,622,623],{"class":93},"\"sk_live_d4dcb310e8b8.\u003Csecret>\"",[83,625,578],{"class":119},[83,627,629],{"class":85,"line":628},8,[83,630,631],{"class":119},"  ]\n",[83,633,635],{"class":85,"line":634},9,[83,636,637],{"class":119},"}\n",[14,639,640],{},"(Shortened: there is one entry per actor.) The secrets appear only here. Core stores hashes of actor secrets and an encrypted copy of signing key secrets, so they cannot be shown again. Running the script a second time imports a second demo project with new credentials, rather than failing.",[14,642,643,644,647,648,651,652,50],{},"App and room slugs are used exactly as written in the document, so topics are addressed as ",[26,645,646],{},"chat/general/messages",". An actor bound to an access scope addresses ",[26,649,650],{},"chat/acme/general/messages","; it may also leave the scope out and kraken inserts it. See ",[18,653,655],{"href":654},"/docs/scopes","Access Scopes",[52,657,659],{"id":658},"connect-a-client","Connect a client",[14,661,662,663,665],{},"From a new directory next to your ",[26,664,23],{}," checkout:",[74,667,669],{"className":76,"code":668,"language":78,"meta":79,"style":79},"mkdir core-client && cd core-client\nnpm init -y\nnpm install @nolag/js-sdk\n",[26,670,671,687,698],{"__ignoreMap":79},[83,672,673,676,679,682,684],{"class":85,"line":86},[83,674,675],{"class":89},"mkdir",[83,677,678],{"class":93}," core-client",[83,680,681],{"class":119}," && ",[83,683,110],{"class":97},[83,685,686],{"class":93}," core-client\n",[83,688,689,692,695],{"class":85,"line":107},[83,690,691],{"class":89},"npm",[83,693,694],{"class":93}," init",[83,696,697],{"class":97}," -y\n",[83,699,700,702,705],{"class":85,"line":116},[83,701,691],{"class":89},[83,703,704],{"class":93}," install",[83,706,707],{"class":93}," @nolag/js-sdk\n",[14,709,710,711,714,715,718],{},"Save this as ",[26,712,713],{},"connect.mjs"," and run it with ",[26,716,717],{},"node connect.mjs",":",[74,720,724],{"className":721,"code":722,"filename":713,"language":723,"meta":79,"style":79},"language-js shiki shiki-themes github-light github-dark","import { readFileSync } from \"node:fs\";\nimport { NoLag } from \"@nolag/js-sdk\";\n\nconst creds = JSON.parse(readFileSync(\"../nolag-core/quickstart/credentials.json\", \"utf8\"));\nconst token = (ref) => creds.actors.find((a) => a.ref === ref).accessToken;\nconst url = \"ws://localhost:8410/ws\";\n\nconst bob = NoLag(token(\"bob\"), { url });\nbob.on(\"chat/general/messages\", (data) => console.log(\"bob got:\", data));\nbob.on(\"connect\", () => {\n  bob.subscribe(\"chat/general/messages\", (err) => {\n    console.log(err ? `subscribe failed: ${err.message}` : \"bob is subscribed\");\n  });\n});\nawait bob.connect();\n\nconst alice = NoLag(token(\"alice\"), { url });\nawait alice.connect();\nalice.emit(\"chat/general/messages\", { text: \"hello from alice\" }, (err) => {\n  console.log(err ? `publish failed: ${err.message}` : \"alice published\");\n});\n","js",[26,725,726,743,757,763,801,846,860,864,889,926,946,970,1006,1012,1018,1033,1038,1060,1072,1102,1132],{"__ignoreMap":79},[83,727,728,731,734,737,740],{"class":85,"line":86},[83,729,730],{"class":123},"import",[83,732,733],{"class":119}," { readFileSync } ",[83,735,736],{"class":123},"from",[83,738,739],{"class":93}," \"node:fs\"",[83,741,742],{"class":119},";\n",[83,744,745,747,750,752,755],{"class":85,"line":107},[83,746,730],{"class":123},[83,748,749],{"class":119}," { NoLag } ",[83,751,736],{"class":123},[83,753,754],{"class":93}," \"@nolag/js-sdk\"",[83,756,742],{"class":119},[83,758,759],{"class":85,"line":116},[83,760,762],{"emptyLinePlaceholder":761},true,"\n",[83,764,765,768,771,774,777,779,782,785,788,790,793,795,798],{"class":85,"line":303},[83,766,767],{"class":123},"const",[83,769,770],{"class":97}," creds",[83,772,773],{"class":123}," =",[83,775,776],{"class":97}," JSON",[83,778,50],{"class":119},[83,780,781],{"class":89},"parse",[83,783,784],{"class":119},"(",[83,786,787],{"class":89},"readFileSync",[83,789,784],{"class":119},[83,791,792],{"class":93},"\"../nolag-core/quickstart/credentials.json\"",[83,794,262],{"class":119},[83,796,797],{"class":93},"\"utf8\"",[83,799,800],{"class":119},"));\n",[83,802,803,805,808,810,812,816,819,822,825,828,831,833,835,837,840,843],{"class":85,"line":581},[83,804,767],{"class":123},[83,806,807],{"class":89}," token",[83,809,773],{"class":123},[83,811,258],{"class":119},[83,813,815],{"class":814},"s4XuR","ref",[83,817,818],{"class":119},") ",[83,820,821],{"class":123},"=>",[83,823,824],{"class":119}," creds.actors.",[83,826,827],{"class":89},"find",[83,829,830],{"class":119},"((",[83,832,18],{"class":814},[83,834,818],{"class":119},[83,836,821],{"class":123},[83,838,839],{"class":119}," a.ref ",[83,841,842],{"class":123},"===",[83,844,845],{"class":119}," ref).accessToken;\n",[83,847,848,850,853,855,858],{"class":85,"line":587},[83,849,767],{"class":123},[83,851,852],{"class":97}," url",[83,854,773],{"class":123},[83,856,857],{"class":93}," \"ws://localhost:8410/ws\"",[83,859,742],{"class":119},[83,861,862],{"class":85,"line":595},[83,863,762],{"emptyLinePlaceholder":761},[83,865,866,868,871,873,876,878,881,883,886],{"class":85,"line":628},[83,867,767],{"class":123},[83,869,870],{"class":97}," bob",[83,872,773],{"class":123},[83,874,875],{"class":89}," NoLag",[83,877,784],{"class":119},[83,879,880],{"class":89},"token",[83,882,784],{"class":119},[83,884,885],{"class":93},"\"bob\"",[83,887,888],{"class":119},"), { url });\n",[83,890,891,894,897,899,902,905,908,910,912,915,918,920,923],{"class":85,"line":634},[83,892,893],{"class":119},"bob.",[83,895,896],{"class":89},"on",[83,898,784],{"class":119},[83,900,901],{"class":93},"\"chat/general/messages\"",[83,903,904],{"class":119},", (",[83,906,907],{"class":814},"data",[83,909,818],{"class":119},[83,911,821],{"class":123},[83,913,914],{"class":119}," console.",[83,916,917],{"class":89},"log",[83,919,784],{"class":119},[83,921,922],{"class":93},"\"bob got:\"",[83,924,925],{"class":119},", data));\n",[83,927,929,931,933,935,938,941,943],{"class":85,"line":928},10,[83,930,893],{"class":119},[83,932,896],{"class":89},[83,934,784],{"class":119},[83,936,937],{"class":93},"\"connect\"",[83,939,940],{"class":119},", () ",[83,942,821],{"class":123},[83,944,945],{"class":119}," {\n",[83,947,949,952,955,957,959,961,964,966,968],{"class":85,"line":948},11,[83,950,951],{"class":119},"  bob.",[83,953,954],{"class":89},"subscribe",[83,956,784],{"class":119},[83,958,901],{"class":93},[83,960,904],{"class":119},[83,962,963],{"class":814},"err",[83,965,818],{"class":119},[83,967,821],{"class":123},[83,969,945],{"class":119},[83,971,973,976,978,981,984,987,989,991,994,997,1000,1003],{"class":85,"line":972},12,[83,974,975],{"class":119},"    console.",[83,977,917],{"class":89},[83,979,980],{"class":119},"(err ",[83,982,983],{"class":123},"?",[83,985,986],{"class":93}," `subscribe failed: ${",[83,988,963],{"class":119},[83,990,50],{"class":93},[83,992,993],{"class":119},"message",[83,995,996],{"class":93},"}`",[83,998,999],{"class":123}," :",[83,1001,1002],{"class":93}," \"bob is subscribed\"",[83,1004,1005],{"class":119},");\n",[83,1007,1009],{"class":85,"line":1008},13,[83,1010,1011],{"class":119},"  });\n",[83,1013,1015],{"class":85,"line":1014},14,[83,1016,1017],{"class":119},"});\n",[83,1019,1021,1024,1027,1030],{"class":85,"line":1020},15,[83,1022,1023],{"class":123},"await",[83,1025,1026],{"class":119}," bob.",[83,1028,1029],{"class":89},"connect",[83,1031,1032],{"class":119},"();\n",[83,1034,1036],{"class":85,"line":1035},16,[83,1037,762],{"emptyLinePlaceholder":761},[83,1039,1041,1043,1046,1048,1050,1052,1054,1056,1058],{"class":85,"line":1040},17,[83,1042,767],{"class":123},[83,1044,1045],{"class":97}," alice",[83,1047,773],{"class":123},[83,1049,875],{"class":89},[83,1051,784],{"class":119},[83,1053,880],{"class":89},[83,1055,784],{"class":119},[83,1057,555],{"class":93},[83,1059,888],{"class":119},[83,1061,1063,1065,1068,1070],{"class":85,"line":1062},18,[83,1064,1023],{"class":123},[83,1066,1067],{"class":119}," alice.",[83,1069,1029],{"class":89},[83,1071,1032],{"class":119},[83,1073,1075,1078,1081,1083,1085,1088,1091,1094,1096,1098,1100],{"class":85,"line":1074},19,[83,1076,1077],{"class":119},"alice.",[83,1079,1080],{"class":89},"emit",[83,1082,784],{"class":119},[83,1084,901],{"class":93},[83,1086,1087],{"class":119},", { text: ",[83,1089,1090],{"class":93},"\"hello from alice\"",[83,1092,1093],{"class":119}," }, (",[83,1095,963],{"class":814},[83,1097,818],{"class":119},[83,1099,821],{"class":123},[83,1101,945],{"class":119},[83,1103,1105,1108,1110,1112,1114,1117,1119,1121,1123,1125,1127,1130],{"class":85,"line":1104},20,[83,1106,1107],{"class":119},"  console.",[83,1109,917],{"class":89},[83,1111,980],{"class":119},[83,1113,983],{"class":123},[83,1115,1116],{"class":93}," `publish failed: ${",[83,1118,963],{"class":119},[83,1120,50],{"class":93},[83,1122,993],{"class":119},[83,1124,996],{"class":93},[83,1126,999],{"class":123},[83,1128,1129],{"class":93}," \"alice published\"",[83,1131,1005],{"class":119},[83,1133,1135],{"class":85,"line":1134},21,[83,1136,1017],{"class":119},[74,1138,1143],{"className":1139,"code":1141,"language":1142,"meta":79},[1140],"language-text","bob is subscribed\nalice published\nbob got: { text: 'hello from alice' }\n","text",[26,1144,1141],{"__ignoreMap":79},[14,1146,1147],{},"The other rules in the demo project hold too. We checked each of these with the quickstart's own tokens:",[182,1149,1150,1162],{},[185,1151,1152],{},[188,1153,1154,1156,1159],{},[191,1155,405],{},[191,1157,1158],{},"Subscribes to",[191,1160,1161],{},"Result",[198,1163,1164,1185,1203,1216,1229,1241],{},[188,1165,1166,1170,1175],{},[203,1167,1168],{},[26,1169,420],{},[203,1171,1172],{},[26,1173,1174],{},"ops/control/alerts",[203,1176,1177,1178,1181,1182,1184],{},"Refused, ",[26,1179,1180],{},"unknown_topic"," (42940): ",[26,1183,380],{}," is restricted and alice has no grant",[188,1186,1187,1191,1196],{},[203,1188,1189],{},[26,1190,420],{},[203,1192,1193],{},[26,1194,1195],{},"chat/vip/messages",[203,1197,1177,1198,1200,1201,373],{},[26,1199,1180],{},": the room is private to ",[26,1202,372],{},[188,1204,1205,1209,1213],{},[203,1206,1207],{},[26,1208,441],{},[203,1210,1211],{},[26,1212,1174],{},[203,1214,1215],{},"Accepted",[188,1217,1218,1222,1226],{},[203,1219,1220],{},[26,1221,441],{},[203,1223,1224],{},[26,1225,1195],{},[203,1227,1228],{},"Accepted, through the type grant",[188,1230,1231,1235,1239],{},[203,1232,1233],{},[26,1234,465],{},[203,1236,1237],{},[26,1238,650],{},[203,1240,1215],{},[188,1242,1243,1247,1251],{},[203,1244,1245],{},[26,1246,465],{},[203,1248,1249],{},[26,1250,646],{},[203,1252,1253],{},"Accepted: kraken inserts the actor's scope",[14,1255,1256,1257,1259,1260,1263,1264,50],{},"To connect a browser with a client token instead, sign a JWT with the ",[26,1258,498],{}," signing key from ",[26,1261,1262],{},"credentials.json",", as shown in ",[18,1265,1266],{"href":502},"Client Tokens",[52,1268,1270],{"id":1269},"verify-the-stack","Verify the stack",[14,1272,1273,1274,1277,1278,1280],{},"The repository carries an acceptance suite that drives the running stack with a real ",[26,1275,1276],{},"@nolag/js-sdk"," client: authentication, a pub/sub round trip, restricted apps, private rooms, and isolation between the two tenants. In the ",[26,1279,23],{}," directory:",[74,1282,1284],{"className":76,"code":1283,"language":78,"meta":79,"style":79},"npm ci\nnpm run test:stack\n",[26,1285,1286,1293],{"__ignoreMap":79},[83,1287,1288,1290],{"class":85,"line":86},[83,1289,691],{"class":89},[83,1291,1292],{"class":93}," ci\n",[83,1294,1295,1297,1300],{"class":85,"line":107},[83,1296,691],{"class":89},[83,1298,1299],{"class":93}," run",[83,1301,1302],{"class":93}," test:stack\n",[14,1304,1305],{},"Against the stack above, all 19 tests passed. The suite starts by checking that kraken refuses a token core never issued, so a broker accidentally left on its static token file fails the run instead of passing it.",[52,1307,1309],{"id":1308},"reconnects","Reconnects",[14,1311,1312,1313,1316,1317,1320,1321,1323,1324,1326],{},"We restarted kraken with ",[26,1314,1315],{},"docker compose restart kraken"," while clients were connected. They reconnected by themselves, but kraken did not restore their subscriptions. The quickstart runs kraken with ",[26,1318,1319],{},"CONTROL_BACKEND=noop",", so kraken never tells core what a connection subscribed to, and core has nothing to hand back on reconnect. A client that subscribes in its ",[26,1322,1029],{}," handler, as ",[26,1325,713],{}," does, carried on receiving messages.",[52,1328,1330],{"id":1329},"stop-and-clean-up","Stop and clean up",[74,1332,1334],{"className":76,"code":1333,"language":78,"meta":79,"style":79},"docker compose down      # stop, and keep the database\ndocker compose down -v   # stop, and delete the database volume\n",[26,1335,1336,1350],{"__ignoreMap":79},[83,1337,1338,1341,1344,1347],{"class":85,"line":86},[83,1339,1340],{"class":89},"docker",[83,1342,1343],{"class":93}," compose",[83,1345,1346],{"class":93}," down",[83,1348,1349],{"class":292},"      # stop, and keep the database\n",[83,1351,1352,1354,1356,1358,1361],{"class":85,"line":107},[83,1353,1340],{"class":89},[83,1355,1343],{"class":93},[83,1357,1346],{"class":93},[83,1359,1360],{"class":97}," -v",[83,1362,1363],{"class":292},"   # stop, and delete the database volume\n",[52,1365,1367],{"id":1366},"what-the-quickstart-is-not","What the quickstart is not",[14,1369,1370],{},"Every item here is a deliberate omission, listed so nobody discovers it the hard way:",[57,1372,1373,1383,1391,1397,1403,1412],{},[60,1374,1375,1378,1379,1382],{},[38,1376,1377],{},"No authentication"," on the example host, and ",[38,1380,1381],{},"no TLS"," anywhere.",[60,1384,1385,1390],{},[38,1386,1387,1388],{},"Secrets in a plaintext ",[26,1389,147],{},", generated by the script and readable only by you. That is not secret management.",[60,1392,1393,1396],{},[38,1394,1395],{},"Postgres in a container"," with a local volume, and no backups, replication or tuning.",[60,1398,1399,1402],{},[38,1400,1401],{},"One kraken node."," No clustering or failover; a kraken restart drops every connection.",[60,1404,1405,258,1408,1411],{},[38,1406,1407],{},"Message recording off",[26,1409,1410],{},"RECORD_MESSAGES=false","). kraken has no history API either way.",[60,1413,1414,1417,1418,1421,1422,1425],{},[38,1415,1416],{},"An admin UI with no accounts",", because the host behind it has none. It lists, reads, creates (from a project document) and deletes projects. Set ",[26,1419,1420],{},"CORS_ORIGINS"," if you serve it from another origin; ",[26,1423,1424],{},"*"," is ignored.",[52,1427,1429],{"id":1428},"running-core-in-your-own-host","Running core in your own host",[14,1431,1432],{},"The library is the deliverable; the example host is about two hundred lines that show the wiring. A host of your own does three things:",[140,1434,1435,1445,1455],{},[60,1436,1437,1440,1441,1444],{},[38,1438,1439],{},"Owns the database connection."," Pass core's entities and migrations to your TypeORM DataSource as the exported arrays, not a glob: a glob relative to your own source never reaches into ",[26,1442,1443],{},"node_modules",", and core's tables would silently not exist.",[60,1446,1447,1450,1451,1454],{},[38,1448,1449],{},"Mounts core"," with ",[26,1452,1453],{},"CoreModule.forRoot({ signingKeyEncryptionKey, defaultLimits })",". Core reads no environment variables and opens no connections of its own.",[60,1456,1457,1460,1461,1464,1465,1468,1469,262,1472,262,1475,1478],{},[38,1458,1459],{},"Exposes the facades behind your own authentication."," The broker-facing routes call ",[26,1462,1463],{},"AuthzFacade"," and return its results through the exported ",[26,1466,1467],{},"toBroker*"," adapters, which define the wire format kraken reads. Configuration routes call ",[26,1470,1471],{},"ProjectConfigFacade",[26,1473,1474],{},"ActorTokenFacade",[26,1476,1477],{},"SigningKeyFacade"," and the rest.",[74,1480,1484],{"className":1481,"code":1482,"language":1483,"meta":79,"style":79},"language-typescript shiki shiki-themes github-light github-dark","import { Module } from \"@nestjs/common\";\nimport { TypeOrmModule } from \"@nestjs/typeorm\";\nimport { CoreModule, allCoreMigrations, coreEntities } from \"@nolag/core\";\n\n@Module({\n  imports: [\n    TypeOrmModule.forRoot({\n      type: \"postgres\",\n      url: process.env.DATABASE_URL,\n      entities: [...coreEntities],\n      migrations: [...allCoreMigrations],\n      migrationsRun: true, // apply core's migrations at startup\n    }),\n    CoreModule.forRoot({ signingKeyEncryptionKey: process.env.SIGNING_KEY_ENCRYPTION_KEY }),\n  ],\n})\nexport class HostModule {}\n","typescript",[26,1485,1486,1500,1514,1528,1532,1543,1548,1558,1568,1578,1589,1599,1612,1617,1632,1636,1641],{"__ignoreMap":79},[83,1487,1488,1490,1493,1495,1498],{"class":85,"line":86},[83,1489,730],{"class":123},[83,1491,1492],{"class":119}," { Module } ",[83,1494,736],{"class":123},[83,1496,1497],{"class":93}," \"@nestjs/common\"",[83,1499,742],{"class":119},[83,1501,1502,1504,1507,1509,1512],{"class":85,"line":107},[83,1503,730],{"class":123},[83,1505,1506],{"class":119}," { TypeOrmModule } ",[83,1508,736],{"class":123},[83,1510,1511],{"class":93}," \"@nestjs/typeorm\"",[83,1513,742],{"class":119},[83,1515,1516,1518,1521,1523,1526],{"class":85,"line":116},[83,1517,730],{"class":123},[83,1519,1520],{"class":119}," { CoreModule, allCoreMigrations, coreEntities } ",[83,1522,736],{"class":123},[83,1524,1525],{"class":93}," \"@nolag/core\"",[83,1527,742],{"class":119},[83,1529,1530],{"class":85,"line":303},[83,1531,762],{"emptyLinePlaceholder":761},[83,1533,1534,1537,1540],{"class":85,"line":581},[83,1535,1536],{"class":119},"@",[83,1538,1539],{"class":89},"Module",[83,1541,1542],{"class":119},"({\n",[83,1544,1545],{"class":85,"line":587},[83,1546,1547],{"class":119},"  imports: [\n",[83,1549,1550,1553,1556],{"class":85,"line":595},[83,1551,1552],{"class":119},"    TypeOrmModule.",[83,1554,1555],{"class":89},"forRoot",[83,1557,1542],{"class":119},[83,1559,1560,1563,1566],{"class":85,"line":628},[83,1561,1562],{"class":119},"      type: ",[83,1564,1565],{"class":93},"\"postgres\"",[83,1567,534],{"class":119},[83,1569,1570,1573,1576],{"class":85,"line":634},[83,1571,1572],{"class":119},"      url: process.env.",[83,1574,1575],{"class":97},"DATABASE_URL",[83,1577,534],{"class":119},[83,1579,1580,1583,1586],{"class":85,"line":928},[83,1581,1582],{"class":119},"      entities: [",[83,1584,1585],{"class":123},"...",[83,1587,1588],{"class":119},"coreEntities],\n",[83,1590,1591,1594,1596],{"class":85,"line":948},[83,1592,1593],{"class":119},"      migrations: [",[83,1595,1585],{"class":123},[83,1597,1598],{"class":119},"allCoreMigrations],\n",[83,1600,1601,1604,1607,1609],{"class":85,"line":972},[83,1602,1603],{"class":119},"      migrationsRun: ",[83,1605,1606],{"class":97},"true",[83,1608,262],{"class":119},[83,1610,1611],{"class":292},"// apply core's migrations at startup\n",[83,1613,1614],{"class":85,"line":1008},[83,1615,1616],{"class":119},"    }),\n",[83,1618,1619,1622,1624,1627,1629],{"class":85,"line":1014},[83,1620,1621],{"class":119},"    CoreModule.",[83,1623,1555],{"class":89},[83,1625,1626],{"class":119},"({ signingKeyEncryptionKey: process.env.",[83,1628,151],{"class":97},[83,1630,1631],{"class":119}," }),\n",[83,1633,1634],{"class":85,"line":1020},[83,1635,584],{"class":119},[83,1637,1638],{"class":85,"line":1035},[83,1639,1640],{"class":119},"})\n",[83,1642,1643,1646,1649,1652],{"class":85,"line":1040},[83,1644,1645],{"class":123},"export",[83,1647,1648],{"class":123}," class",[83,1650,1651],{"class":89}," HostModule",[83,1653,1654],{"class":119}," {}\n",[14,1656,1657,1658,1661,1662,1665,1666,1669,1670,1674,1675,50],{},"Then point kraken at your broker-facing routes with ",[26,1659,1660],{},"AUTH_BACKEND=http"," and ",[26,1663,1664],{},"AUTH_HTTP_URL",", and set ",[26,1667,1668],{},"BACKEND_SECRET"," to the bearer secret your routes require. The routes the example host serves, and the document it imports, are described in ",[18,1671,1673],{"href":1672},"/docs/self-hosting/project-document","Project Document",". The contract kraken expects is in ",[18,1676,1678],{"href":1677},"/docs/self-hosting/http-auth","HTTP Auth Contract",[52,1680,1682],{"id":1681},"next-steps","Next steps",[57,1684,1685,1689,1693],{},[60,1686,1687],{},[18,1688,1673],{"href":1672},[60,1690,1691],{},[18,1692,1266],{"href":502},[60,1694,1695],{},[18,1696,1698],{"href":1697},"/docs/self-hosting/production","Production Checklist",[1700,1701,1702],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":79,"searchDepth":107,"depth":107,"links":1704},[1705,1706,1707,1708,1709,1710,1711,1712,1713,1714],{"id":54,"depth":107,"text":55},{"id":71,"depth":107,"text":72},{"id":309,"depth":107,"text":310},{"id":658,"depth":107,"text":659},{"id":1269,"depth":107,"text":1270},{"id":1308,"depth":107,"text":1309},{"id":1329,"depth":107,"text":1330},{"id":1366,"depth":107,"text":1367},{"id":1428,"depth":107,"text":1429},{"id":1681,"depth":107,"text":1682},"Run kraken, @nolag/core's example host, Postgres and the admin UI on your machine with one script, connect with the seeded demo tokens, and verify the stack with its test suite.","md",{},"/docs/self-hosting/full-stack",{"title":5,"description":1715},"docs/self-hosting/full-stack","P0r8A_2gmdFfzYA25u6OqDjE0_HJ9lo2lcLpxe19EVg",1791536074856]