SQLAdmin — panel administracyjny dla SQLAlchemy i FastAPI
FastAPI nie ma panelu administracyjnego w pudełku, a Django i Laravel mają. SQLAdmin wypełnia tę lukę deklaratywnymi klasami ModelView nad istniejącymi modelami SQLAlchemy — z filtrami, importem CSV strumieniującym postęp, dziennikiem audytu i jednorazowym pokazywaniem sekretów. Omawiamy też haczyki na zapytania, bez których panel wielodostępny wycieknie danymi, oraz fakt, że domyślnie nie ma żadnego uwierzytelniania.
Kto pracował w Django, ten wie, że panel administracyjny jest tam czymś, o czym się nie myśli — po prostu jest. My w Laravelu mamy Filamenta i Novę. FastAPI nie ma niczego w pudełku i to jest świadoma decyzja projektowa: framework dostarcza routing, walidację i dokumentację, a wszystko inne jest do złożenia z bibliotek.
Problem pojawia się wtedy, gdy klient prosi o „prosty panel do wpisywania danych" nad serwisem, który ma już modele SQLAlchemy i pięćdziesiąt endpointów. Budowanie do tego frontendu jest przerostem. Postawienie obok Django tylko dla panelu — jeszcze większym.
SQLAdmin (github.com/smithyhq/sqladmin) jest odpowiedzią na dokładnie ten scenariusz: deklaratywny panel nad istniejącymi modelami SQLAlchemy, wpinany w aplikację Starlette albo FastAPI jednym wywołaniem konstruktora. Autorzy nie ukrywają genealogii — konfiguracja jest wzorowana na Flask-Adminie i większość opcji nazywa się tak samo.
Stan projektu
Dane z API GitHuba i PyPI na 28 września 2026:
- 2821 gwiazdek i 297 forków, repozytorium założone 22 grudnia 2021 — blisko pięć lat rozwoju,
- wersja 0.31.1 z 1 września; sześć wydań między początkiem lipca a początkiem września, więc tempo jest wysokie i stałe,
- Licencja BSD 3-Clause, Python 3.10–3.14, wymagane SQLAlchemy 2.0+ i Starlette 0.50+,
- 53 otwarte zgłoszenia, ostatni push dzień przed napisaniem tego wpisu,
- interfejs zbudowany na Tablerze, formularze na WTForms, szablony w Jinja2. Obsługiwane są zarówno silniki synchroniczne, jak i asynchroniczne SQLAlchemy, a także modele SQLModel,
- jest publiczne demo na żywo z podanymi w README danymi logowania, więc ocena wyglądu i ergonomii zajmuje minutę, bez stawiania czegokolwiek.
Dwie rzeczy trzeba przy tym powiedzieć wprost, bo w planie tej serii projekt figuruje jeszcze pod starym adresem. Repozytorium przeniosło się z konta aminalaee do organizacji smithyhq, utworzonej 31 marca 2026. Organizacja ma trzy publiczne repozytoria: SQLAdmina, fastapi-storages i aplikację demonstracyjną — czyli jest to porządkowe przeniesienie projektów jednego autora pod wspólny szyld, nie przejęcie przez firmę. Nie ma tam ani opisu, ani strony, ani zapowiedzi zmiany modelu.
Druga rzecz jest ważniejsza dla decyzji o wdrożeniu: to projekt jednego autora. Amin Alaee ma 269 commitów, kolejny człowiek w zestawieniu — 25, a trzeci największy „kontrybutor" to Dependabot. Kod jest utrzymany, testowany i wydawany regularnie, ale ryzyko jest takie samo jak w każdym projekcie z jednym maintainerem, a nie takie, jakie sugeruje przynależność do organizacji.
Sześć linii do działającego panelu
Instalacja rozdziela funkcje na dodatki, co warto docenić — nie ciągnie zależności, których nie użyjesz:
pip install sqladmin # rdzeń
pip install "sqladmin[auth]" # + itsdangerous, sesyjne uwierzytelnianie
pip install "sqladmin[i18n]" # + babel, tłumaczenia interfejsu
pip install "sqladmin[full]" # obaPodpięcie do FastAPI wygląda tak i to jest całość konfiguracji minimalnej:
from fastapi import FastAPI
from sqladmin import Admin, ModelView
app = FastAPI()
admin = Admin(app, engine)
class UserAdmin(ModelView, model=User):
column_list = [User.id, User.name]
admin.add_view(UserAdmin)Pod /admin jest już pełny CRUD z listą, szczegółami, tworzeniem, edycją i usuwaniem. Dla Starlette różnica to jedna linia — zamiast FastAPI() jest Starlette().
Sam konstruktor Admin przyjmuje przy tym więcej, niż widać w quickstarcie: base_url (domyślnie /admin), title, logo_url z wymiarami, favicon_url, templates_dir, własne middlewares, authentication_backend, i18n_config i audit_backend. Zamiast engine można podać session_maker — i to jest droga do wielu baz danych, bo fabryka sesji SQLAlchemy z binds rozdziela modele między silniki, a SQLAdmin po prostu jej używa. Ta sama ścieżka służy do zachowania własnej konfiguracji sesji.
ModelView jako deklaracja
Cała konfiguracja panelu to atrybuty klasy. Uprawnienia są sześcioma flagami:
can_create,can_edit,can_delete,can_view_details,can_export— domyślnie włączone,can_import— domyślnie wyłączone, i to jest właściwa decyzja domyślna dla operacji masowej.
Do tego metadane (name, name_plural, icon z FontAwesome albo Tablera, category grupujące widoki w rozwijanym menu), opcje listy, szczegółów, paginacji (page_size, page_size_options) i formatowania. Kategorie w menu startują zwinięte, a stan zwinięcia jest pamiętany per instancja panelu w localStorage przeglądarki, więc przetrwa nawigację między stronami.
Kolumny da się wskazywać obiektami, nazwami, specjalnym "__all__" — i ścieżkami z kropką przez relacje, na przykład "address.zip_code". Ta ostatnia możliwość jest wygodna i dokumentacja od razu ostrzega, czym za nią płacisz: relacja zostanie doładowana, co wywoła dodatkowe zapytania dla każdego wiersza. Uczciwie postawione, więc nie ma niespodzianki na produkcji.
Formatery są dwuwarstwowe i to rozwiązanie warte zapamiętania także poza Pythonem. column_formatters działa na konkretne kolumny, a column_type_formatters — na typy, więc jedną deklaracją formatujesz wszystkie daty albo wszystkie None w całym panelu. Oba mają warianty _detail dla strony szczegółów, a jeśli wariantu nie podasz, używany jest ten od listy. W paczce są gotowe formatery, w tym copy_to_clipboard_formatter — praktyczny do kolumn UUID, których nikt nie przepisuje ręcznie.
Filtry: protokół, nie hierarchia klas
column_filters przyjmuje obiekty spełniające protokół ColumnFilter, a protokół jest tak mały, że własny filtr napiszesz w kilkunastu linijkach. Wymagane są dwa pola i dwie metody: title (nagłówek w prawym pasku), parameter_name (klucz w URL-u), lookups() zwracające listę par klucz–etykieta oraz get_filtered_query() zwracające przefiltrowane zapytanie.
W module sqladmin.filters są filtry gotowe:
BooleanFilter— Tak/Nie dla kolumn logicznych,AllUniqueStringValuesFilter— wszystkie unikalne wartości kolumny jako klikalne linki,StaticValuesFilter— to samo, ale z listą podaną z góry, bez pytania bazy,ForeignKeyFilter— po kluczu obcym, z wskazaniem pola z modelu powiązanego, które ma być etykietą,OperationColumnFilter— najciekawszy, bo sam rozpoznaje typ kolumny i dobiera operacje: zawiera / równa się / zaczyna się / kończy się dla tekstu, równa się / większe / mniejsze dla liczb i dat, a dla kolumn UUID (SQLAlchemy 2.0+) zawiera / równa się / zaczyna się.
Dokumentacja podaje przy tym jasne kryterium wyboru, którego zwykle brakuje: filtry pokazujące wszystkie wartości jako linki są dobre dla kolumn o małej liczbie unikalnych wartości, a OperationColumnFilter z rozwijaną listą operacji i polem tekstowym — dla kolumn o dużej kardynalności oraz dla liczb i dat. To istotne, bo AllUniqueStringValuesFilter na kolumnie z milionem różnych wartości zapyta bazę o wszystkie.
Haczyki na zapytania — bez nich panel wielodostępny przecieka
To jest według mnie najważniejszy fragment API SQLAdmina i jednocześnie ten, który najłatwiej przeoczyć, bo w dokumentacji stoi jako punkt na liście opcji listy. Pięć metod pozwala przepisać zapytania, na których stoi cały panel:
list_query(request)— zapytanie strony listy,count_query(request)— zapytanie liczące, do paginacji,search_query(stmt, term)— zapytanie wyszukiwania,details_query(request)— zapytanie strony szczegółów,form_edit_query(request)— dane wczytywane do formularza edycji.
Każde z nich dostaje request, a więc i sesję. Tu, i tylko tu, zakłada się ograniczenie widoczności do danych jednego klienta, jednego oddziału czy jednego właściciela. Panel nie ma osobnej warstwy uprawnień na poziomie wiersza — ma te haczyki. Jeśli budujesz panel, w którym różne osoby mają widzieć różne wiersze tej samej tabeli, trzeba nadpisać wszystkie pięć: przefiltrowana lista przy nieprzefiltrowanych szczegółach albo formularzu edycji to wyciek danych przez wpisanie identyfikatora w adres.
Uzupełnieniem są dwie metody z sekcji uwierzytelniania, działające na poziomie całego widoku: is_visible(request) decyduje o pokazaniu pozycji w menu, a is_accessible(request) o dostępie. Dokumentacja podkreśla, że do wyświetlenia w pasku obie muszą zwrócić prawdę — i warto pamiętać, że is_visible samo w sobie niczego nie chroni, bo ukrycie linku nie jest kontrolą dostępu.
Formularze i pułapka relacji
Formularze stoją na WTForms i mają kilkanaście opcji, z których trzy używa się praktycznie zawsze. form_columns i form_excluded_columns wybierają pola. form_create_rules i form_edit_rules pozwalają dać różne zestawy pól przy tworzeniu i przy edycji — na tym stoi cała obsługa haseł, o której niżej. form_overrides podmienia typ pola (na przykład na wtforms.EmailField), form_args i form_widget_args przekazują argumenty do pól i widżetów, a form_include_pk włącza klucz główny do formularza, domyślnie z niego wyłączony.
I teraz pułapka, na którą wpadnie każdy, kto pierwszy raz otworzy panel na prawdziwej bazie. Relacje są w formularzu renderowane jako zwykły select, do którego ładowane są wszystkie rekordy powiązanej tabeli. Przy kilkuset wierszach jest wolno, przy kilkuset tysiącach strona edycji przestaje się otwierać. Dokumentacja mówi to wprost: praktycznie przy każdym żądaniu strony edycji wczytywane są wszystkie rekordy tabeli powiązanej.
Rozwiązaniem jest form_ajax_refs, który zamienia listę na wyszukiwanie przez AJAX z Select2:
class ParentAdmin(ModelView, model=Parent):
form_ajax_refs = {
"children": {
"fields": ("id",),
"order_by": "id",
}
}Przyjmuje też warunek where, którym da się od razu odciąć rekordy, które nie powinny być w ogóle wybieralne — na przykład archiwalne albo należące do innego klienta. Traktowałbym form_ajax_refs jako element konfiguracji obowiązkowy dla każdej relacji do tabeli, która rośnie, a nie jako optymalizację na później.
Hasła i sekrety jednorazowe
Dwa wzorce, które w panelach administracyjnych robi się źle wyjątkowo często, mają tu porządne odpowiedzi.
Hasło jako pole tylko przy tworzeniu. Przepis z dokumentacji składa się z trzech elementów: column_labels przemianowuje hashed_password na czytelne „password", form_create_rules włącza je do formularza tworzenia, form_edit_rules pomija w formularzu edycji, a haszowanie ląduje w on_model_change pod warunkiem is_created. Efekt: hasło ustawia się raz, nigdy nie wraca do formularza i nigdy nie trafia do bazy w postaci jawnej.
Sekret pokazywany dokładnie raz. Klucz API generowany w panelu powinien być widoczny natychmiast po utworzeniu i nigdy więcej. Służy do tego Secret.reveal_once(request, value) wywoływane z after_model_change: SQLAdmin pomija zwykłe przekierowanie, renderuje stronę ponownie i pokazuje wartość w jednorazowym okienku modalnym.
Konstrukcja tego mechanizmu jest tym, co warto skopiować niezależnie od stosu: sekret żyje wyłącznie na request.state — nie jest zapisywany w sesji ani wysyłany w ciasteczku, więc nie może wyciec między żądaniami. Wbudowane obsługi tworzenia i edycji stemplują odpowiedź nagłówkami zakazującymi cache'owania automatycznie; własne widoki muszą zrobić to same, o czym dokumentacja uprzedza.
Eksport i import CSV
Eksport jest domyślnie włączony, obsługuje CSV i JSON (export_types), pozwala wybrać kolumny i ograniczyć liczbę wierszy. Flaga use_pretty_export włącza wariant, w którym plik dostaje etykiety kolumn i sformatowane wartości takie same jak w interfejsie — czyli to, co dostaje osoba, która chce ten plik czytać, a nie wczytać z powrotem.
Import jest wyłączony domyślnie i zaprojektowany zauważalnie starannie:
- endpoint strumieniuje postęp jako NDJSON (
application/x-ndjson), gdzie każda linia to obiekt typuprogressalboresult. Duży plik nie kończy się więc zawieszoną przeglądarką bez informacji, - limity są jawne:
max_import_file_size(domyślnie 5 MB),import_max_rows,max_reported_missed_rows(domyślnie 100), - pole
continue_on_errordecyduje, czy błędne wiersze są pomijane, czy przerywają import, - rozdzielone są dwie różne liczby:
skippedto pełna liczba pominiętych wierszy, amissed_rowsto szczegółowe raporty przycięte do limitu, zmissed_rows_omitted_countmówiącym, ilu nie pokazano. Dokumentacja podsumowuje to jednym zdaniem, które powinno być standardem: sumy są kompletne, szczegółowy podgląd może być z projektu przycięty, - błędy uploadu (brak pliku, złe rozszerzenie, typ zawartości, kodowanie, przekroczony limit) wracają jako zwykły tekst z kodem 400 albo 413, a nie jako NDJSON — więc obsługa błędów w kliencie jest jednoznaczna,
- znacznik BOM na początku pliku UTF-8 jest przyjmowany i cicho zdejmowany, co oszczędza godzinę zgadywania przy plikach z Excela.
Są przy tym dwie rzeczy, które trzeba wiedzieć, zanim włączysz import w produkcji. Pierwsza: nagłówki CSV muszą używać nazw właściwości modelu, nie etykiet — więc plik wyeksportowany z use_pretty_export=True nie wczyta się z powrotem bez zmiany nagłówków albo jawnego column_import_list. Druga jest poważniejsza: on_model_change i after_model_change nie są wywoływane w trakcie importu CSV. Cała logika, którą tam umieściłeś — haszowanie haseł, normalizacja, powiadomienia — po prostu się nie wykona. Do tego celu jest osobny haczyk on_import_row(data, model, request) oraz check_can_import(request) do dynamicznego blokowania przycisku i endpointu. Sama dokumentacja dodaje przy column_import_list, że domyślną wartością są kolumny listy albo klucz główny i że w produkcji należy ustawić to jawnie.
Dziennik audytu
Funkcja, której w tej kategorii narzędzi zwykle nie ma, a bez której panel administracyjny w projekcie z kilkoma osobami jest ślepy. Admin(audit_backend=...) rejestruje każde utworzenie, zmianę i usunięcie wykonane przez panel. Domyślny backend to NullAuditBackend, czyli audyt jest w pełni opcjonalny i nic nie zapisuje, dopóki go nie włączysz.
Wpis ma stały kształt: dataklasa AuditEntry z polami action („create", „update", „delete"), identity (identyfikator widoku), pk, changes (przesłane wartości pól; dla usunięć puste) i timestamp w UTC. Do wyboru są trzy drogi:
LoggingAuditBackend— wypycha wpisy przez standardowy modułlogging, pod loggersqladmin.audit. Jedna linia konfiguracji i ślad trafia tam, gdzie już zbierasz logi,DBAuditBackend— zapisuje do tabeli, ale SQLAdmin świadomie nie dostarcza modelu audytu. Uzasadnienie jest w dokumentacji i jest dobre: właściwy kształt zależy od typu klucza głównego użytkowników (int, str czy UUID) i od klucza obcego do nich. Definiujesz więc własny model i nadpisujesz dwie metody —get_actor(request)mapującą sesję na identyfikator aktora ibuild_row(entry, actor, request)budującą wiersz. PolaAuditEntrynigdy się nie zmieniają, zmienia się tylko to mapowanie,- własny
AuditBackendz jedną metodąasync log(entry, request)— do kolejki komunikatów albo zewnętrznej usługi audytowej.
Dwa detale wykonania decydują o tym, czy to się nadaje do produkcji, i oba są zrobione dobrze. Audyt jest best-effort: backend, który rzuci wyjątkiem, jest logowany, a nie propagowany — źle skonfigurowany dziennik nie zepsuje już zatwierdzonej zmiany. I DBAuditBackend zapisuje we własnej transakcji, osobnej od transakcji zmiany, którą opisuje. Skoro tabela audytu jest po prostu jednym z Twoich modeli, można ją zarejestrować jako ModelView tylko do odczytu i przeglądać ślad w panelu, z normalnymi filtrami i stroną szczegółów.
Uwierzytelnianie — i najczęstszy błąd wdrożenia
Dokumentacja otwiera stronę o uwierzytelnianiu zdaniem, które trzeba zapamiętać: SQLAdmin nie wymusza żadnego uwierzytelniania, tylko dostarcza opcjonalny AuthenticationBackend. Znaczy to dokładnie tyle, ile znaczy — panel dodany trzema linijkami z quickstartu jest otwarty dla każdego, kto zna adres. To najczęstszy i najpoważniejszy błąd wdrożenia tego narzędzia.
Backend sesyjny wymaga nadpisania trzech metod: authenticate (wołane przy każdym żądaniu), login (tylko na stronie logowania) i logout. Zwracane wartości mają jasno opisaną semantykę — True daje domyślne przekierowanie, False renderuje szablon logowania z kodem 400 i komunikatem, a zwrócenie obiektu Response jest przekazywane bez zmian. Do działania potrzebny jest pakiet itsdangerous, czyli dodatek [auth].
Integracja z OAuth też jest opisana i sprowadza się do dwóch zmian w tym samym backendzie: w authenticate zwracasz await google.authorize_redirect(...), gdy w sesji nie ma użytkownika, a wymianę kodu na token obsługuje własna trasa dorzucona do admin.app.router. Przykład w dokumentacji używa Authlib i metadanych OpenID Connect Google'a — więc podłączenie panelu do firmowego dostawcy tożsamości jest kwestią kilkudziesięciu linii, nie przepisywania warstwy uwierzytelniania.
Tłumaczenia
Dodatek [i18n] włącza tłumaczenie interfejsu przez Babel. W paczce są skompilowane katalogi dla azerskiego, niemieckiego, angielskiego, rosyjskiego i tureckiego — polskiego nie ma, więc panel po polsku wymaga własnego katalogu.
Kolejność ustalania języka jest udokumentowana i sensowna: parametr ?lang= z przełącznika (utrwalany w ciasteczku), potem ciasteczko, potem nagłówek Accept-Language negocjowany względem obsługiwanych języków, a na końcu default_locale. Przyjmowane są wyłącznie języki z SUPPORTED_LOCALES. Detal, który zapobiega kilkunastu minutom szukania: jeśli podasz i18n_config, a babel nie jest zainstalowany, dostajesz UserWarning, nie błąd — interfejs po prostu zostaje nieprzetłumaczony.
Pułapki
- Brak uwierzytelniania domyślnie. Bez
authentication_backendpanel jest otwarty. To pierwsza rzecz do skonfigurowania, nie ostatnia, is_visiblenie chroni niczego — ukrywa pozycję w menu. Kontrolą dostępu jestis_accessible, a ograniczeniem widoczności wierszy — haczyki na zapytania,- Filtrowanie musi objąć wszystkie pięć zapytań. Przefiltrowana lista przy nieprzefiltrowanych szczegółach to wyciek przez adres URL,
- Relacje w formularzach ładują całe tabele.
form_ajax_refsjest wymogiem, nie optymalizacją, - Ścieżki z kropką i
"__all__"generują dodatkowe zapytania na każdą relację i każdy wiersz listy, - Import pomija
on_model_changeiafter_model_change— logika z tych metod nie wykona się ani razu. Do importu jeston_import_row, - Domyślna lista kolumn importu to kolumny listy albo klucz główny; dokumentacja sama każe ustawić
column_import_listjawnie w produkcji, - Pretty export nie wczytuje się z powrotem — nagłówki muszą być nazwami właściwości modelu,
AllUniqueStringValuesFilterpyta bazę o wszystkie unikalne wartości. Do kolumn o dużej kardynalności właściwy jestOperationColumnFilter,- Wersja 0.31.x — projekt jest przed 1.0, przy tempie kilku wydań na kwartał. Wersję warto przypiąć,
- Jeden autor odpowiada za blisko 90% commitów, a przeniesienie do organizacji tego nie zmienia,
- Brak polskiego katalogu tłumaczeń, a brak
babelprzy podanej konfiguracji i18n daje tylko ostrzeżenie, - Komunikaty flash bez sesji są cicho ignorowane — bez
SessionMiddlewarepowiadomienia po akcjach po prostu nie pojawią się, bez żadnego błędu, - Wymagane SQLAlchemy 2.0+ i Starlette 0.50+. Serwis stojący na SQLAlchemy 1.4 wymaga migracji ORM-a, zanim w ogóle dojdzie do panelu.
Gdzie to ma sens
- Panel wewnętrzny nad istniejącym serwisem FastAPI. Modele są, baza jest, brakuje interfejsu dla dwóch osób z zespołu klienta. To jest scenariusz, w którym SQLAdmin daje najwięcej za najmniej,
- Wprowadzanie i poprawianie danych tam, gdzie budowanie własnego frontendu nigdy nie zwróciłoby się z budżetu,
- Przeglądanie śladu audytu — własna tabela plus widok tylko do odczytu daje filtrowalny dziennik zmian bez pisania żadnego interfejsu,
- Panel operacyjny z akcjami własnymi. Dekorator
actiondodaje przyciski na liście i w szczegółach, z opcjonalnym pytaniem o potwierdzenie w okienku modalnym i powiadomieniamiFlash.successpo wykonaniu. Zatwierdzanie wniosków, ponawianie wysyłek, przeliczanie — bez dokładania endpointów.
Czego SQLAdmin nie jest: panelem dla klientów końcowych ani systemem uprawnień. Uprawnienia są tu flagami na widok i haczykami na zapytania — wystarczy dla zespołu, w którym wszyscy mają w zasadzie ten sam dostęp albo różnice da się wyrazić jednym warunkiem. Jeśli potrzebujesz ról, zakresów i historii uprawnień, budujesz to sam obok, a nie w SQLAdminie.
Podsumowanie
SQLAdmin jest dobrym przykładem biblioteki, która robi jedną rzecz i nie próbuje zostać frameworkiem. Co warto zapamiętać:
- Sześć linii daje działający CRUD nad istniejącymi modelami SQLAlchemy, w FastAPI i w Starlette, na silnikach synchronicznych i asynchronicznych,
- Konfiguracja to atrybuty klasy, wzorowane na Flask-Adminie — kto go zna, nie uczy się nowego API,
- Skonfiguruj uwierzytelnianie w pierwszym kroku, bo domyślnie nie ma żadnego,
- Panel wielodostępny wymaga nadpisania wszystkich pięciu zapytań: listy, licznika, wyszukiwania, szczegółów i formularza edycji,
form_ajax_refsna każdej rosnącej relacji — inaczej strona edycji wczyta całą tabelę powiązaną,- Hasło przez
form_create_rulespluson_model_change, sekrety przezSecret.reveal_oncetrzymane wyłącznie narequest.state, - Import strumieniuje postęp jako NDJSON, ma jawne limity i rozdziela pełne sumy od przyciętego podglądu błędów — ale pomija haczyki zmiany modelu,
- Audyt jest opcjonalny, best-effort i pisze we własnej transakcji, a modelu wpisu świadomie nie dostarcza, bo zależy od typu klucza Twoich użytkowników,
- Filtry to mały protokół, więc własny filtr to kilkanaście linii;
OperationColumnFiltersam dobiera operacje do typu kolumny, - Demo na żywo pozwala ocenić narzędzie przed instalacją.
Licencja: SQLAdmin jest na BSD 3-Clause — licencji permisywnej, w praktyce równoważnej MIT z jednym dodatkiem. Wolno używać komercyjnie, modyfikować, wpinać w zamknięte produkty i redystrybuować; obowiązki sprowadzają się do zachowania noty o prawach autorskich i tekstu licencji w dystrybucji kodu oraz w dokumentacji dystrybucji binarnej. Tym dodatkiem względem MIT jest trzecia klauzula: zakaz używania nazwy projektu i nazwisk autorów do promowania produktów pochodnych bez pisemnej zgody. Dla agencji stawiającej panel klientowi nie zmienia to nic w kodzie, ale zmienia coś w materiałach handlowych — „panel oparty na SQLAdminie" jako opis techniczny jest w porządku, natomiast budowanie wizerunku produktu na nazwie biblioteki albo nazwisku jej autora wymagałoby zgody. Nie ma tu wersji płatnej, funkcji za paywallem ani umowy licencyjnej dla kontrybutorów. Rozsądniejsze niż analiza licencji jest w tym przypadku pilnowanie wersji: projekt jest przed 1.0 i wydaje kilka razy na kwartał, więc zakres wersji w pliku zależności warto przypiąć ciaśniej, niż zrobiłbyś to dla biblioteki stabilnej.