---
title: "Appwrite vs Supabase: bezpečnost, stejné útoky na oba"
description: "Stejné přístupové útoky jsem pustil na Appwrite 2.3.0 a lokální Supabase. Obojí zamyká nové tabulky; liší se tím, jak selže chyba a co otevře klíč."
url: "https://madsalt.tech/cs/blog/appwrite-vs-supabase/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/cs/about/"
language: "cs"
published: 2026-10-30
---

# Appwrite vs Supabase: bezpečnost, stejné útoky na oba

TL;DR

Obojí ve výchozím stavu zamyká novou tabulku: Appwrite odpověděl `401` hostovi i přihlášenému uživateli a Supabase odpovídá `401`/`403`, dokud tabulku nepřidělíte rolím API. Rozdíl se ukáže, když je pravidlo špatně nebo když požadavek přechází mezi uživateli. Na Supabase vrátila úprava cizího řádku `200 []` a příliš volná politika pro vložení nechala uživatele připojit se do jiné firmy; na Appwrite skončil každý pokus napříč uživateli v mém testu jako `401` nebo `404` a uživatel nemohl svůj řádek přidělit nikomu jinému než sobě nebo všem. Serverové klíče Appwrite mají scopes; secret key Supabase přeskakuje všechny politiky.

Jsem Denys, full-stack a AI inženýr. Většina aplikací, které stavím nebo opravuji, běží na Supabase, a klienti, kteří si backend teprve vybírají, se ptají, jak si Appwrite stojí v bezpečnosti. Seznam funkcí na to neodpoví, tak jsem oběma poslal stejné požadavky.

**Jak jsem testoval.** Appwrite 2.3.0, self-hosted v Dockeru, 30. října 2026, přes jeho REST API jako host, jako dva přihlášení uživatelé A a B a se dvěma serverovými klíči. Strana Supabase je můj test na lokálním stacku (CLI 2.109.1, Postgres 17.6) z 24. října 2026 se stejnými rolemi, podrobně popsaný v článku [Příklady politik Supabase RLS](https://madsalt.tech/cs/blog/supabase-rls-policy-examples/). Na obou vymyšlené řádky.

## Jak Appwrite a Supabase chrání data?

Supabase chrání tabulky přes Postgres: grant rozhoduje, zda se role API k tabulce vůbec dostanou, a politiky row level security, psané v SQL, rozhodují, které řádky každý požadavek vidí a zapisuje. Appwrite ukládá oprávnění vedle dat: tabulka má seznam rolí, které smějí číst, vytvářet, upravovat nebo mazat, a se zapnutou row security nese každý řádek vlastní seznam. Podle [dokumentace oprávnění Appwrite](https://appwrite.io/docs/products/databases/tablesdb/permissions) se přístup jen uděluje, nikdy neodpírá, a uživatel ho potřebuje na úrovni tabulky nebo řádku, ne obojího.

Otázka tedy nezní, který z nich má řízení přístupu. Mají ho oba. Je to v tom, v čem vám každý dovolí udělat chybu.

## Co dělá každý s novou tabulkou?

| Požadavek                        | Appwrite, bez nastavených oprávnění               | Supabase, RLS zapnuté, bez grantu (24. 10.) |
| -------------------------------- | ------------------------------------------------- | ------------------------------------------- |
| Host vypíše řádky                | `401` user_unauthorized                           | `401` permission denied                     |
| Přihlášený uživatel vypíše řádky | `401` user_unauthorized                           | `403` permission denied                     |
| Přihlášený uživatel přidá řádek  | `401` No permissions provided for action 'create' | netestováno                                 |

Čerstvá tabulka je na obou uzavřená. Na Supabase je to novinka: tabulky vytvořené po 30. říjnu 2026 už automaticky nedostávají granty Data API, jak jsem popsal v článku [změna grantů 30. října](https://madsalt.tech/cs/blog/supabase-october-30-grant-change/).

## Co se stane, když je tabulka otevřená všem?

Dvě „otevřená“ nastavení nejsou totéž. V Appwrite jsem tabulce dal jediné oprávnění, `read("any")`. Na Supabase je běžný otevřený stav RLS vypnuté s výchozími granty.

| Požadavek hosta | Appwrite, `read("any")` | Supabase, RLS vypnuté, výchozí granty (24. 10.) |
| --------------- | ----------------------- | ----------------------------------------------- |
| Výpis řádků     | `200`, oba řádky        | `200`, oba řádky                                |
| Smazání řádku   | `401`                   | `200`, oba řádky smazány                        |

„Veřejné“ v Appwrite znamená jen čtení, dokud nepřidělíte i zápis. Tabulku Supabase s vypnutým RLS může číst i zapisovat kdokoli s publishable key, proto je „RLS disabled in public“ první věc, kterou kontroluji.

## Jak fungují „vlastní řádky“ v každém?

V Appwrite jsem zapnul row security a tabulce dal jediné oprávnění, `create("users")`. Když A vytvořil řádek bez uvedení oprávnění, Appwrite je nastavil sám: `read`, `update` a `delete` jen pro uživatele A. Na Supabase je stejný záměr čtyři politiky na `(select auth.uid()) = user_id`.

| Požadavek         | Appwrite (30. 10.)  | Supabase (24. 10.)             |
| ----------------- | ------------------- | ------------------------------ |
| Host vypíše řádky | `200`, 0 řádků      | `401` (žádný grant pro `anon`) |
| A vypíše řádky    | `200`, 2 řádky A    | `200`, 2 řádky A               |
| A čte řádek B     | `404` row_not_found | ve výpisu se nevrátí           |
| A upraví řádek B  | `401`               | `200 []`                       |
| A smaže řádek B   | `401`               | `200 []`                       |

![Stejný požadavek, dva backendy. Uživatel A upravuje řádek uživatele B přes REST API obou. Supabase, politika jen pro vlastníka: Uživatel A → úprava řádku B → 200 \[\] → 0 řádků, bez chyby; Appwrite, row security zapnuté: Uživatel A → úprava řádku B → 401 → vrácena chyba.](https://madsalt.tech/charts/cs/appwrite-vs-supabase.svg)

*Obrázek 1. Moje testy: lokální stack Supabase (CLI 2.109.1), 24. října 2026; self-hosted Appwrite 2.3.0, 30. října 2026.*

Ani jeden nevyzradil řádek B. Liší se odpověď. Supabase filtruje řádky, které politika skrývá, takže úprava, která nic nenašla, je úspěch s nulou řádků; aplikace, která bere „bez chyby“ jako „uloženo“, pak uživateli řekne, že jeho úprava prošla. Appwrite odmítne chybou, kterou aplikace nemůže přehlédnout.

## Může uživatel předat svá data někomu jinému?

Tady generovaný kód chybuje nejčastěji, tak jsem to zkusil na obou.

| Požadavek A                             | Appwrite                                               | Supabase (24. 10.)                                       |
| --------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- |
| Vytvořit řádek čitelný uživatelem B     | `401`, Permissions must be one of: any, users, user:A… | záleží na politice                                       |
| Změnit vlastní řádek, aby ho vlastnil B | `401`, stejná zpráva                                   | `403` s úplným `with check`; `200` s `with check (true)` |
| Vytvořit řádek čitelný všemi            | `201` a host ho pak vypsal                             | jen pokud to politika dovolí                             |

Appwrite omezuje, co může uživatel udělit, na role, které sám drží: `any`, `users`, svého uživatele a své týmy. To zablokovalo krok „dej můj řádek někomu jinému“, aniž bych napsal jediné pravidlo. Nezabránilo to A zveřejnit vlastní řádek všem přes `read("any")`. Pokud řádek nesmí být nikdy veřejný, neměl by oprávnění nastavovat klient; vytvořte ho ze serverové funkce.

Na Supabase je výsledek takový, jak říká politika. Úplný `with check` přesun odmítl s `403`; `with check (true)`, které píší někteří generátory, nechal A přesunout řádek na účet B.

## Může se uživatel připojit do cizí firmy?

Na Supabase je přístup týmů obvykle tabulka `memberships` plus politiky. V mém testu insert politika, kterou navrhuje řada tutoriálů, `with check (user_id = auth.uid())`, nechala A přidat sebe do firmy 2 s `201 Created` a pak číst její projekt s výplatami.

Appwrite má týmy zabudované. B vytvořil tým; A se do něj zkusil přidat jako člen i jako vlastník. Oba požadavky vrátily `404 team_not_found`: A tým ani neviděl. O členství v týmu rozhoduje server, ne pravidlo, které píšete, takže tento konkrétní únik není možný. Řádky pořád potřebují oprávnění `read("team:…")`, aby se sdílely, a ta nastavujete sami.

## Co otevírá serverový klíč?

| Klíč                              | Appwrite                              | Supabase (z mého testu anon vs service role) |
| --------------------------------- | ------------------------------------- | -------------------------------------------- |
| Plný serverový klíč               | `200`, všechny 4 řádky obou uživatelů | všechny 3 řádky, RLS přeskočeno              |
| Klíč s úzkým scope (`users.read`) | `401`, missing scope rows.read        | žádné klíče se scope                         |

Secret key Supabase odpovídá roli `service_role`, která má `BYPASSRLS`; co otevírá, je v článku [anon key vs service role key](https://madsalt.tech/cs/blog/supabase-anon-key-vs-service-role-key/). Klíč Appwrite také ignoruje oprávnění řádků, ale jen pro scopes, se kterými vznikl, takže klíč pro úlohu spravující uživatele nepřečte vaše tabulky. Jakýkoli klíč v balíčku pro prohlížeč je plný únik všeho, k čemu se dostane.

Dva menší nálezy v Appwrite: host i přihlášený uživatel, kteří žádali o seznam tabulek, dostali `401` (volání vyžaduje `tables.read`, které mají jen klíče), a `read("users")` znamená jakýkoli přihlášený účet, stejná past jako `auth.uid() is not null` na Supabase, když je registrace otevřená.

## Který je bezpečnější?

Ani jeden není ve výchozím stavu bezpečný nebo nebezpečný; oba začaly uzavřené. Liší se tím, kde se může chyba schovat:

| Otázka                                    | Appwrite                                 | Supabase                                   |
| ----------------------------------------- | ---------------------------------------- | ------------------------------------------ |
| Kde žijí pravidla přístupu                | Seznamy rolí u tabulek a řádků           | SQL granty a politiky                      |
| Odepřený zápis napříč uživateli           | Chyba (`401` / `404`)                    | `200` s nulou řádků u update a delete      |
| Přidělování řádků jiným                   | Jen rolím, které uživatel drží           | Cokoli, co politika dovolí                 |
| Pravidla podle hodnot sloupců, úroveň MFA | Nejsou v oprávněních; chce serverový kód | V politice, např. `aal2` pro citlivé řádky |
| Serverové klíče                           | Se scopes                                | Secret key přeskakuje všechny politiky     |

Model Appwrite je užší a v mém testu odmítl každý trik napříč uživateli, aniž bych cokoli psal. Model Supabase je SQL, takže vyjádří pravidla, která oprávnění Appwrite neumí, třeba „jen s MFA“ nebo „jen řádky firem, do kterých patřím“, a vyjádří i špatné pravidlo. Pokud váš tým politiky číst nebude, Appwrite ve výchozím stavu odpouští víc. Pokud potřebujete pravidla závislá na datech, Supabase vám je dovolí napsat, a pak je nutné je testovat; metoda, kterou používám, je v článku [jak testovat Supabase RLS](https://madsalt.tech/cs/blog/how-to-test-supabase-rls/).

## Jak zkontrolovat projekt na Supabase?

Bezplatná [Kontrola zabezpečení Supabase](https://madsalt.tech/cs/tools/supabase-security-check/) čte váš projekt jen pro čtení a označí tabulky s vypnutým RLS, politiky, které platí vždy, zápisové politiky pro hosty a granty, které dávají víc, než politiky naznačují. Co Supabase řeší za vás a co nechává na vás, je v článku [je Supabase bezpečný?](https://madsalt.tech/cs/blog/is-supabase-secure/). Pro Appwrite nic takového není; spusťte výše uvedené požadavky jako dva uživatelé. Pro Firebase jsem stejný druh útoků pustil v článku [příklady pravidel zabezpečení Firebase](https://madsalt.tech/cs/blog/firebase-security-rules-examples/).

**Limity.** Jeden self-hosted Appwrite 2.3.0 na notebooku a jeden lokální stack Supabase, s odstupem šesti dnů, vymyšlené řádky. Appwrite Cloud se může lišit od self-hosted. Storage, funkce, realtime, nastavení autentizace a certifikáty shody se tu neporovnávají.

Vybíráte backend pro aplikaci se soukromými daty?

Rád se s vámi podívám na váš model přístupu. Bez nabídky.

[Ozvěte se](https://madsalt.tech/cs/#contact)
