Tworzenie, aktualizacja i uruchamianie aplikacji webowych Noe przez API.
Jak robimy aplikacje Noe (skrót)
- Aplikacja to rekord z polem
source_codew wybranym silniku:svelte(domyślny, Svelte 5 z$state),vue,react,js,html; CSS przezcss_engine: "tailwind_all". Serwer sam kompiluje kod -
url_codedaje przyjazny adres (np. “pogoda” →/a/pogoda),public: trueudostępnia apkę bez logowania - Dane trzymamy w bazie Noe (
db_code+ publiczne metody bazy) albo pobieramy z zewnętrznych API przez konektory - Logika backendowa idzie przez akcje aplikacji: wpis w
actionswskazuje flow (flow_code), frontend wołaPOST /noe/apps/:id/action/:nazwa, a flow wykonuje resztę (konektory, transformacje, warunki). Frontend nigdy nie woła zewnętrznych API bezpośrednio -
Bezpieczeństwo: NIGDY nie wpisuj
api_tokenani sekretów wsource_code— kod aplikacji jest widoczny dla użytkowników. Do danych publicznych używajpublic_methodsbazy Noe albo konektora z metodamipublic: true - Poprawki kodu rób przez
PATCH /noe/apps/:id/replace.json(field: "source_code",old_string/new_string) zamiast przesyłać cały kod od nowa
Przykład: prosta apka z custom connectorem, custom flow i cronem
Wzorzec “pobieraj cyklicznie z zewnętrznego API, przy danej wartości zareaguj, pokazuj wynik w apce”:
-
Custom connector (
kind: "noe/custom_connector") do zewnętrznego API - metody HTTP deklarujesz wfields.methods(path z interpolacją{nazwa}), bazowy adres wurl, klucz API wsecret_token -
Custom flow (
kind: "noe/custom_flow") z programem wfields.program: krokconnector(pobranie danych) →mutation(wyciągnięcie pól) →ifz warunkiem na wyniku (np."{json_object.temperature} > 30"), a w gałęzithenkolejny krokconnectorodpalający inne API (alert/powiadomienie) -
Cykliczna akcja konta budząca flow według harmonogramu:
POST /account/crons.json { "cron": { "kind": "flow", "target_code": "KOD_FLOW", "schedule": "*/30 * * * *", "active": true } }Flow musi mieć ustawiony
code. Cron przekazuje na wejściu{ "kind": "cron", "cron_id": ... }- parametry startowe (np. współrzędne) ustaw pierwszym krokiemmutation. Przebiegi programu sprawdzisz w/connect/flow_processes, cron ręcznie odpalisz przyciskiem “Uruchom teraz” w/account/crons -
Aplikacja Noe z akcją
{ "action": "get_data", "flow_code": "KOD_FLOW", "public": true }- frontend woła akcję (POST /noe/apps/:id/action/get_data) i renderuje dane; cron w tle dba o cykliczne odświeżanie/alerty
Autoryzacja: Authorization: Bearer TOKEN
Content-Type: application/json
API Endpoints
| Metoda | Sciezka | Opis |
|---|---|---|
| GET | /noe/apps.json |
Lista aplikacji |
| GET | /noe/apps/:id.json |
Szczegoly aplikacji |
| POST | /noe/apps.json |
Utworzenie aplikacji |
| PATCH | /noe/apps/:id.json |
Aktualizacja |
| DELETE | /noe/apps/:id.json |
Usuniecie |
| GET | /a/:url_code |
Uruchomienie aplikacji (publiczny URL) |
| PATCH | /noe/apps/:id/replace.json |
Czesciowa edycja pola |
Pola aplikacji
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
name |
string | tak | Nazwa aplikacji |
url_code |
string | nie | Przyjazny URL slug (np. “kalkulator” -> /a/kalkulator) |
description |
string | nie | Opis aplikacji |
app_engine |
string | tak | Silnik: svelte, vue, react, js, html, kaboom, phaser
|
css_engine |
string | tak | CSS: tailwind_all (CDN), tailwind (kompilowany), none
|
source_code |
string | tak | Kod zrodlowy (max 200 000 znakow) |
public |
boolean | nie |
true = dostepna bez logowania (domyslnie false) |
active |
boolean | nie | Czy aplikacja jest aktywna (domyslnie true) |
db_code |
string | nie | Kod bazy Noe::Db (wiele aplikacji moze wspoldzielic jedna baze) |
show_in_module |
string | nie | Osadza aplikacje w module (np. organize, mail, crm) |
dashboard_widget |
boolean | nie | Apka na liscie “Dodaj kafelki” pulpitow Toolkit (zapis w fields.dashboard_widget, reszta fields zostaje). Bez tego apki nie da sie dodac jako kafelek |
prompt |
string | nie | Prompt systemowy dla AI |
actions |
array | nie | Definicje akcji do integracji z Flow |
fields |
object | nie | Pola uzytkownika (JSON) |
Silniki aplikacji
| Silnik | Opis | Kompilacja | CSS |
|---|---|---|---|
svelte |
Svelte 5 z $state/$derived
|
tak (serwer kompiluje) | tak |
vue |
Vue 3 SFC (Composition API) | tak | tak |
react |
React 19 z JSX | tak | tak |
js |
Czysty JavaScript | nie (source_code = app_code) | nie |
html |
Czysty HTML | nie (source_code = app_code) | nie |
kaboom |
Kaboom.js (gry 2D) | tak | nie |
phaser |
Phaser (gry 2D/3D) | tak | nie |
Silniki CSS
| Silnik | Opis | Kiedy uzywac |
|---|---|---|
tailwind_all |
Tailwind CSS przez CDN (<script src="cdn.tailwindcss.com">) |
Domyslny — zawsze dziala, wiekszy rozmiar strony |
tailwind |
Tailwind kompilowany na serwerze — mniejszy rozmiar | Dla produkcyjnych apek (tylko uzywane klasy) |
none |
Bez CSS frameworka | Dla apek bez stylowania lub z wlasnym CSS |
Utworzenie aplikacji
POST /noe/apps.jsonContent-Type: application/json
{
"app": {
"name": "Nazwa aplikacji",
"url_code": "kod-url",
"description": "Opis",
"app_engine": "svelte",
"css_engine": "tailwind_all",
"public": true,
"source_code": "KOD_ZRODLOWY"
}
}
Sukces — POST (201 Created)
{
"id": "019cb0d4-47be-7ea0-b3e1-ea8bec016f89",
"name": "Nazwa aplikacji",
"url_code": "kod-url",
"description": "Opis",
"app_engine": "svelte",
"css_engine": "tailwind_all",
"source_code": "...",
"public": true,
"active": true,
"app_build_time": 79,
"css_build_time": 0,
"fields": {},
"config": {},
"actions": [],
"db_code": null,
"show_in_module": null,
"created_by_id": 1,
"updated_by_id": null,
"created_at": "2026-03-03T00:13:47.964+01:00",
"updated_at": "2026-03-03T00:13:47.964+01:00"
}
Uwaga: Odpowiedz NIE zawiera: account_id, app_code, css_code, token.
Blad walidacji (422 Unprocessable Content)
{
"name": ["can't be blank"],
"app_engine": ["is not included in the list"],
"source_code": ["blad kompilacji: ..."]
}
Aktualizacja aplikacji
PATCH /noe/apps/:id.jsonContent-Type: application/json
{
"app": {
"source_code": "NOWY_KOD"
}
}
Mozna wyslac tylko zmienione pola — reszta pozostaje bez zmian.
Replace API (czesciowa edycja pola)
Pozwala zmienic fragment source_code, fields lub prompt bez przesylania calego pola:
PATCH /noe/apps/:id/replace.jsonContent-Type: application/json
{
"field": "source_code",
"old": "let count = $state(0)",
"new": "let count = $state(10)"
}
URL aplikacji
Aplikacja jest dostepna pod adresem:
-
/a/:url_code— publiczny URL (np./a/kalkulator) -
/a/:id— URL po UUID (np./a/019cb0d4-47be-7ea0-b3e1-ea8bec016f89)
Pelny URL: https://{host}/a/{url_code}
Wazne: URL to /a/ (nie /noe/).
Svelte 5 — skladnia
Svelte 5 uzywa “runes” — nowy system reaktywnosci:
<script>
// Stan reaktywny
let count = $state(0);
let items = $state([]);
let name = $state("");
// Wartosc wyliczana (automatycznie aktualizowana)
let doubled = $derived(count * 2);
let total = $derived(items.length);
// Efekt (uruchamiany przy zmianie zaleznosci)
$effect(() => {
console.log("Count zmieniony:", count);
});
// Funkcje
function increment() {
count++;
}
function addItem() {
items = [...items, { name }];
name = "";
}
</script>
<!-- Template -->
<button onclick={increment}>Count: {count} (doubled: {doubled})</button>
<input bind:value={name} placeholder="Nowy element" />
<button onclick={addItem}>Dodaj</button>
{#each items as item, i}
<div>{i + 1}. {item.name}</div>
{/each}
{#if count > 10}
<p>Duzo!</p>
{:else}
<p>Malo</p>
{/if}
Kluczowe roznice Svelte 5 vs Svelte 4
| Svelte 4 | Svelte 5 |
|---|---|
let count = 0; |
let count = $state(0); |
$: doubled = count * 2; |
let doubled = $derived(count * 2); |
$: { console.log(count); } |
$effect(() => { console.log(count); }); |
on:click={handler} |
onclick={handler} |
export let prop; |
let { prop } = $props(); |
Vue 3 — skladnia
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">Count: {{ count }} ({{ doubled }})</button>
</template>
React 19 — skladnia
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
);
}
createRoot(document.getElementById("noe-app")).render(<App />);
JS — skladnia
// Czysty JS — kod uruchamiany bezposrednio w przegladarce
// Kontener: document.getElementById("noe-app")
(function() {
const container = document.getElementById("noe-app");
container.innerHTML = '<h1>Hello World</h1>';
})();
Kontekst aplikacji (window.NoeAppContext)
Aplikacje osadzone w modulach maja dostep do danych uzytkownika:
{
user: {
login: "jan.kowalski",
email: "[email protected]",
name: "Jan Kowalski",
role: "admin", // admin, manager, user, guest
team_ids: [1, 2],
team_names: ["Sprzedaz", "IT"],
group_ids: [5],
group_names: ["Menedzerowie"],
department_ids: [3],
department_names: ["Dzial IT"]
}
}
Osadzanie w modulach (show_in_module)
Pole show_in_module osadza aplikacje w module systemu (menu boczne, breadcrumbs).
Dostepne moduly: organize, mail, crm, kb, cms, connect, voip, form, insight, billing, drive, fiskator, commerce, account
Wstrzykiwanie na strony: show_in_module moze zawierac sciezke do strony (np. tasks/show). Wtedy apka jest wstrzykiwana bezposrednio w widok.
| Wartosc | Zachowanie |
|---|---|
organize |
Aplikacja w menu bocznym modulu |
tasks/show |
Wstrzyknieta na stronie szczegulow zadania |
contacts/index |
Wstrzyknieta na liscie kontaktow |
Akcje (integracja z Flow)
Akcje pozwalaja wywolywac Flow z frontendu:
{
"app": {
"actions": [
{
"action": "get_data",
"flow_code": "my-data-flow",
"description": "Pobiera dane z API",
"public": true
}
]
}
}
Wywolanie z frontendu:
const response = await fetch('/noe/apps/APP_ID/action/get_data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ param1: "value1" })
});
const result = await response.json();
// result = { flow_process_id: "...", status: "pending"|"finished", data: {...} }
Polling statusu (dla dlugich operacji):
async function waitForResult(appId, actionName, flowProcessId) {
while (true) {
const response = await fetch(`/noe/apps/${appId}/action/${actionName}/status?flow_process_id=${flowProcessId}`);
const result = await response.json();
if (result.status === "finished") return result.data;
if (result.status === "error") throw new Error(result.error);
await new Promise(r => setTimeout(r, 1000));
}
}
Bazy danych Noe (Noe::Db) — podsumowanie
Aplikacje moga korzystac z prostych baz danych Noe. Pełne API baz: noe_db_api.
Utworzenie bazy
POST /noe/dbs.json
{
"db": {
"name": "Todo",
"code": "todo",
"schema": {
"fields": {
"title": { "type": "string", "required": true },
"done": { "type": "integer" }
},
"indexes": {
"idx_str_1": "title",
"idx_int_1": "done"
}
}
}
}
Dostepne indeksy
| Indeks | Typ |
|---|---|
idx_str_1, idx_str_2, idx_str_3
|
string (max 255 znakow) |
idx_int_1, idx_int_2
|
bigint |
idx_time_1, idx_time_2
|
datetime |
Typy pol w schema
string, text, integer, number, boolean, datetime, date, time
Baza wielotabelowa
Jedna baza moze trzymac kilka tabel — zamiast fields na gornym poziomie podaj tables,
a kazda tabela dostaje wlasne fields i wlasne indexed_fields (pelny limit indeksow per tabela):
{
"schema": {
"tables": {
"users": { "fields": { "name": { "type": "string", "required": true }, "email": "string" },
"indexed_fields": ["name", "email"] },
"orders": { "fields": { "order_number": "string", "user_id": "number" },
"indexed_fields": ["order_number", "user_id"] }
}
}
}
Rekordy takiej bazy przekazuja table_name ({ "record": { "table_name": "orders", "data": {...} } }),
a wyszukiwanie wymaga ?table_name=orders — bez niego endpoint zwraca 400 z lista dostepnych tabel.
Frontendy appek korzystajace z metod publicznych: table_name dziala tylko w search, wiec do zapisu
do konkretnej tabeli uzyj prywatnego API rekordow albo osobnej bazy jednotabelowej.
Szczegoly: noe_db_api.
Operacje na rekordach (prywatne — wymaga autoryzacji)
const DB = "todo";
// Lista
const records = await fetch(`/noe/dbs/${DB}/records.json`).then(r => r.json());
// Utworzenie
await fetch(`/noe/dbs/${DB}/records.json`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ record: { data: { title: "Zadanie", done: 0 } } })
});
// Aktualizacja
await fetch(`/noe/dbs/${DB}/records/${id}.json`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ record: { data: { done: 1 } } })
});
// Usuniecie
await fetch(`/noe/dbs/${DB}/records/${id}.json`, { method: 'DELETE' });
// Wyszukiwanie
const results = await fetch(`/noe/dbs/${DB}/search.json?q=zadanie&sort=-created_at`).then(r => r.json());
Publiczne metody (bez autoryzacji)
Pole public_methods na bazie udostepnia operacje bez logowania:
{
"public_methods": {
"list": { "scope_by": "user_ip" },
"create": {
"allowed_fields": ["title", "done"],
"required_fields": ["title"],
"default_values": { "done": 0 },
"auto_fields": { "user_ip": "{ip}" }
},
"update": { "allowed_fields": ["title", "done"], "scope_by": "user_ip" },
"delete": { "scope_by": "user_ip" }
}
}
URL publiczny: POST /noe/db/:db_code/:method_name
Zmienne w auto_fields: {ip} (adres IP), {cookie_key} (unikalny token per przegladarka), {user_id} / {user_name} (zalogowany uzytkownik; puste bez sesji)
Roznica: Records API vs Search API
| API | Struktura danych |
|---|---|
Records (/records.json) |
Zagniezdzona: record.data.field
|
Search (/search.json) |
Plaska: record.field
|
Kompletny przyklad — Kalkulator (Svelte 5)
POST /noe/apps.jsonContent-Type: application/json
{
"app": {
"name": "Kalkulator",
"url_code": "kalkulator",
"description": "Prosty kalkulator",
"app_engine": "svelte",
"css_engine": "tailwind_all",
"public": true,
"source_code": "<script>\n let display = $state(\"0\");\n let firstOperand = $state(null);\n let operator = $state(null);\n let waitingForSecondOperand = $state(false);\n\n function inputDigit(digit) {\n if (waitingForSecondOperand) {\n display = String(digit);\n waitingForSecondOperand = false;\n } else {\n display = display === \"0\" ? String(digit) : display + digit;\n }\n }\n\n function inputDecimal() {\n if (waitingForSecondOperand) { display = \"0.\"; waitingForSecondOperand = false; return; }\n if (!display.includes(\".\")) display = display + \".\";\n }\n\n function clear() {\n display = \"0\"; firstOperand = null; operator = null; waitingForSecondOperand = false;\n }\n\n function toggleSign() { display = String(parseFloat(display) * -1); }\n function inputPercent() { display = String(parseFloat(display) / 100); }\n\n function calculate(first, op, second) {\n switch (op) {\n case \"+\": return first + second;\n case \"-\": return first - second;\n case \"*\": return first * second;\n case \"/\": return second !== 0 ? first / second : \"Error\";\n default: return second;\n }\n }\n\n function handleOperator(nextOp) {\n const val = parseFloat(display);\n if (operator && !waitingForSecondOperand) {\n const result = calculate(firstOperand, operator, val);\n display = String(result); firstOperand = result;\n } else { firstOperand = val; }\n waitingForSecondOperand = true; operator = nextOp;\n }\n\n function handleEquals() {\n if (operator == null || waitingForSecondOperand) return;\n const result = calculate(firstOperand, operator, parseFloat(display));\n display = String(result); firstOperand = null; operator = null; waitingForSecondOperand = false;\n }\n</script>\n\n<div class=\"min-h-screen bg-gray-900 flex items-center justify-center p-4\">\n <div class=\"w-full max-w-xs bg-gray-800 rounded-2xl shadow-2xl overflow-hidden\">\n <div class=\"p-6 pb-2\">\n <div class=\"text-right text-white text-5xl font-light tracking-tight truncate min-h-[60px] flex items-end justify-end\">\n {display.length > 9 ? parseFloat(display).toExponential(4) : display}\n </div>\n </div>\n <div class=\"grid grid-cols-4 gap-[1px] p-3\">\n <button onclick={clear} class=\"bg-gray-500 hover:bg-gray-400 text-black rounded-full h-16 text-2xl font-medium\">C</button>\n <button onclick={toggleSign} class=\"bg-gray-500 hover:bg-gray-400 text-black rounded-full h-16 text-2xl font-medium\">+/-</button>\n <button onclick={inputPercent} class=\"bg-gray-500 hover:bg-gray-400 text-black rounded-full h-16 text-2xl font-medium\">%</button>\n <button onclick={() => handleOperator(\"/\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">/</button>\n {#each [7,8,9] as d}<button onclick={() => inputDigit(d)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">{d}</button>{/each}\n <button onclick={() => handleOperator(\"*\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">x</button>\n {#each [4,5,6] as d}<button onclick={() => inputDigit(d)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">{d}</button>{/each}\n <button onclick={() => handleOperator(\"-\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">-</button>\n {#each [1,2,3] as d}<button onclick={() => inputDigit(d)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">{d}</button>{/each}\n <button onclick={() => handleOperator(\"+\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">+</button>\n <button onclick={() => inputDigit(0)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium col-span-2 text-left pl-6\">0</button>\n <button onclick={inputDecimal} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">.</button>\n <button onclick={handleEquals} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">=</button>\n </div>\n </div>\n</div>"
}
}
Wynik: Aplikacja dostepna pod https://{host}/a/kalkulator
Kompletny przyklad — Todo z baza (Svelte 5)
Krok 1: Utworz baze
POST /noe/dbs.json
{
"db": {
"name": "Todo",
"code": "todo",
"schema": {
"fields": {
"title": { "type": "string", "required": true },
"done": { "type": "integer" },
"user_ip": { "type": "string" }
},
"indexes": { "idx_str_1": "title", "idx_str_2": "user_ip", "idx_int_1": "done" }
},
"public_methods": {
"list": { "scope_by": "user_ip" },
"create": {
"allowed_fields": ["title", "done"],
"required_fields": ["title"],
"default_values": { "done": 0 },
"auto_fields": { "user_ip": "{ip}" }
},
"update": { "allowed_fields": ["title", "done"], "scope_by": "user_ip" },
"delete": { "scope_by": "user_ip" }
}
}
}
Krok 2: Utworz aplikacje
POST /noe/apps.json
{
"app": {
"name": "Todo",
"url_code": "todo",
"app_engine": "svelte",
"css_engine": "tailwind_all",
"public": true,
"db_code": "todo",
"source_code": "<script>\n let tasks = $state([]);\n let newTitle = $state(\"\");\n const DB = \"todo\";\n\n async function load() {\n const res = await fetch(`/noe/db/${DB}/list`, { method: \"POST\" });\n tasks = await res.json();\n }\n\n async function add() {\n if (!newTitle.trim()) return;\n await fetch(`/noe/db/${DB}/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: { title: newTitle } })\n });\n newTitle = \"\"; await load();\n }\n\n async function toggle(task) {\n await fetch(`/noe/db/${DB}/update`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id: task.id, data: { done: task.done ? 0 : 1 } })\n });\n await load();\n }\n\n async function remove(id) {\n await fetch(`/noe/db/${DB}/delete`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id })\n });\n await load();\n }\n\n $effect(() => { load(); });\n</script>\n\n<div class=\"max-w-md mx-auto p-6\">\n <h1 class=\"text-2xl font-bold mb-4\">Todo</h1>\n <div class=\"flex gap-2 mb-4\">\n <input bind:value={newTitle} placeholder=\"Nowe zadanie...\" class=\"border p-2 rounded flex-1\" onkeydown={(e) => e.key === 'Enter' && add()} />\n <button onclick={add} class=\"bg-blue-500 text-white px-4 py-2 rounded\">Dodaj</button>\n </div>\n {#each tasks as task}\n <div class=\"flex items-center gap-2 p-2 border-b\">\n <input type=\"checkbox\" checked={task.done === 1} onchange={() => toggle(task)} />\n <span class={task.done === 1 ? 'line-through text-gray-400' : ''}>{task.title}</span>\n <button onclick={() => remove(task.id)} class=\"ml-auto text-red-500\">x</button>\n </div>\n {/each}\n</div>"
}
}
Wskazowki
-
Znaki specjalne w JSON — przy wysylaniu przez curl unikaj em dash i innych znakow UTF-8. Uzywaj ASCII lub przesylaj z pliku (
-d @plik.json) -
Duzy source_code — przygotuj JSON w pliku i uzyj
curl -d @payload.json -
Bledy kompilacji — przy bledzie w source_code serwer zwroci 422 z opisem bledu w polu
source_code - url_code — musi byc unikalny na koncie. Serwer automatycznie oczyci go z niedozwolonych znakow
-
Publiczna apka — ustaw
"public": trueaby byla dostepna bez logowania -
Tailwind — uzywaj
tailwind_all(CDN) dla prostoty lubtailwinddla mniejszego rozmiaru -
Testowanie — po utworzeniu odwiedz
https://{host}/a/{url_code}w przegladarce
Powiązane
- noe_db_api — pełne API baz danych Noe (schema, rekordy, metody publiczne, auto_fields)
- noe_app_example_weather — kompletny przykład krok po kroku: apka z custom connectorem, custom flow i cronem (pogoda)
- common_api — wspólne zasady API (format, autoryzacja, odpowiedzi)