NewClaude over MCP — it proposes, you approve.

Changelog

What’s new

Every release of Forge, Core and the rest — what each one added, changed and fixed, newest first.

  1. Core

    0.4.0

    The licensed compiler arrives — one Bezalel Core licence installs Core and the compiler, and only the compiler builds — with live lists and presence, analytics, search, and a long list of fields, filters and widgets.

    Added
    • TemplateField: a Word template written in OnlyOffice with what it can say beside it — the variables its records offer, searched, copied with a click — and what it has behind it on its page: each ${…} its file uses and what it means, an unknown one marked; «Try it on a record» opens it filled from the latest record to read (forResource(), or variables()/sample()/values()). See docs/guide/document-templates.md. Run bezalel-core:stubs
    • HasDocuments: a record made into a document from a Word template — a «Document» row action choosing a template, showing what each ${…} says for the record (an empty one marked), and downloading it as Word or, through OnlyOffice's ConvertService, PDF. The variables are the Resource's fields unless it names its own (documentVariables(), TemplateVariable); the templates are the app's (documentTemplates(), TemplateFile). Documents\WordTemplate reads and fills .docx variables with no library. Run bezalel-core:stubs
    • Panel::mediaLibrary(false): a panel kept away from the shared media library — a portal for customers or carriers: none of the library's routes on it, its file fields uploading without it, its rich text with no image from it (panel.mediaLibrary shared). Run bezalel-core:stubs
    • Panel::interstitials([...]): a page held until something's done — an Interstitial of the app's names the pages it covers and, per request, whether they're held and what it says; the layout draws it over the page with no way past it (components/interstitial.tsx, shared as interstitial). Run bezalel-core:stubs
    • DateField::minDate()/maxDate() (a day or today: checked when saved, the calendar offering nothing past them), ->withToday() (a «Today» button), ->clearable() (a button to clear it). Compiled by both compilers: only a date field with them compiles differently. Run bezalel-core:stubs
    • PhoneField: a phone input (type="tel"), checked as a number, kept as E.164 (->countryCode('998') for a number typed without one, ->asTyped() to keep it as written, ->requireCountryCode(); ->unchecked() leaves the checking to its own rules), a tel: link in the table; no phone library
    • TextField::mask('999 999 999'): typed into a mask (9 a digit, a a letter, * either), checked against it, kept without what it put in (MaskedInput). Compiled by both compilers: only a masked field compiles differently. Run bezalel-core:stubs
    • Resource::listData(): what a list component draws besides its rows — the page's own settings, a list to choose from — as table.listData, worked out per request. Run bezalel-core:stubs
    • Panel::pageTabs(fn () => [...]): a row of tabs over pages that belong together, drawn by the layout above whichever of them is open (components/page-tabs.tsx, shared as pageTabs), worked out per request. Run bezalel-core:stubs
    • Realtime, which an app turns on (bezalel-core.realtime.enabled, BEZALEL_REALTIME; Laravel's broadcasting on Reverb or Pusher; npm install laravel-echo pusher-js), so bezalel-core:stubs publishes lib/realtime-echo.ts. When on: the bell hears notifications as they're sent (PanelNotification broadcasts); a table reloads on its Resource::liveUpdates() events; useRealtime() in lib/realtime.ts for the app's own components. When off, nothing of it is in the build. Run bezalel-core:stubs
    • Action::form([...fields]): an action asks for fields in a dialog the package draws (ActionForm) — text, a list (an async one searched as @{action}.{field}), a date, yes/no, a file — posted with it, files and all, and checked by the fields' own rules; no component to write. Run bezalel-core:stubs
    • Resource::$exports: the table's look — its filters, search and sort, every row — downloaded as a spreadsheet from {plural}/export (exportAction() in the toolbar, Action::download()): xlsx written by Support\Spreadsheet with no library (CSV without PHP's zip extension); each column's label and what it shows by default, exportRows() for a sheet's own columns. Run bezalel-core:stubs
    • PointField: a point on the map as «lat,lng», typed in or picked on the map (components/forms/LocationPicker, the map's PickerMap) — while the map's on (bezalel-core.map.enabled, shared as bezalelMap); the text alone otherwise. Run bezalel-core:stubs
    • Step::claims('segments.*.hops'): a wizard step answers for keys besides its fields' — one object edited across steps — so a mistake on them takes the wizard to it (the most specific claim wins); a record's wizard opens any step from its title. Compiled by both compilers: only a step that claims keys compiles differently. Run bezalel-core:stubs
    • Resource::$cardColumns: how many cards a row holds on a wide screen, 1–4 (three by default). Run bezalel-core:stubs
    • Filter::defaultValue() on a table: it opens filtered by it — worked out per request (a role's own view), shown chosen — until it's cleared (sent with no condition, defaultFilters in the payload) or another value is picked. Run bezalel-core:stubs
    • Action::component($name, width: '4xl'): the component's dialog wider than the default (md, lg, xl, 2xl, 4xl). Run bezalel-core:stubs
    • Page::routeSlug(): a page names its own address, under a section too (queries/query-map, its route queries.query-map); by default its name, kebab-cased, as before
    • MapWidget and MapLayer: markers in layers on a map — clustered, a coloured pill or an image turned by a heading — a clicked one opening the app's panel, a place searched for (places(), {page}/map-places), the view remembered. Leaflet's, and optional: bezalel-core.map.enabled (BEZALEL_MAP), npm install leaflet leaflet.markercluster, then bezalel-core:stubs publishes components/map/leaflet-map.tsx; off, nothing of it is in the build. bezalel-core:stubs publishes optional stub sets while they're on and removes them (unchanged) when off. Run bezalel-core:stubs
    • SelectFilter::options() and colors() take a closure: options kept in the database worked out per request, sent in the payload (filterOptions) over the compiled page's. Run bezalel-core:stubs
    • Filter::visibleWhen(): a filter offered only to whom it's for (the bin to a Super Admin) — declared always, so the compiled page knows it; neither applied nor shown to the rest (hiddenFilters in the payload). Run bezalel-core:stubs
    • SelectFilter::icons(), colors(): an option's icon and colour on its tab. Resource::$defersTabCounts (DataTable::deferTabCounts()): the tabs' counts follow the page, a deferred prop of their own. Run bezalel-core:stubs
    • Resource::$listComponent: a component of the app's drawing the whole list in place of the generated table (DataTableList, lib/list-components.ts). Compiled by both compilers: only a Resource that names one compiles differently. Run bezalel-core:stubs
    • Resource::views(): a list seen more than one way — cards, the table, a board — switched in the toolbar (DataTableViews, ?view=, the last remembered); Resource::$loadsOnScroll: the next page as the list is scrolled to its end (DataTableLoadMore); Resource::rowsFor() (DataTable::rowsOf()): any records as the table's rows; DataTable::sortedQuery(): a look's whole query. Compiled by both compilers: only a Resource with them compiles differently. Run bezalel-core:stubs
    • SelectFilter::dependsOn(): an async filter whose options follow other filters' values (a region's districts), its search getting them as state, its choice cleared when one changes. Run bezalel-core:stubs
    • Resource::board(): the Index as columns of cards (Board, BoardColumn — a label, colours, a query narrowing the table's), each with its count and first cards and «Ещё» ({plural}/board-column); the table's search, filters, sort, card component and row actions apply in every column. DataTable::withoutRows(). Compiled by both compilers: only a Resource that declares board() compiles differently. Run bezalel-core:stubs
    • Page::filters(): what every widget on a page follows (a month, a manager), the table's Filter classes drawn as a bar above the widgets (components/widgets/PageFilters.tsx) and kept in the address; each widget's data() gets their values (Page::filterValues()), a value picked checked by Filter::pageValue(), else Filter::defaultValue(); a select marked asTabs() is a row of buttons, and Page::filtersWidget() places the bar among the widgets. Compiled by both compilers: only a page with filters compiles differently. Run bezalel-core:stubs
    • Widget::defer(): a slow widget's data loads after the page shows (an Inertia deferred prop of its own); WidgetGrid shows a placeholder until it comes. Run bezalel-core:stubs
    • Resource::ownerKey(): who the signed-in person is as an owner-scoped Resource's $owner column holds it — their id by default; a company's TIN, or a list of keys whose rows are all theirs (a new record filed under the one chosen when it's among them). Null or an empty list owns nothing
    • A wizard step's checkStep() may answer with a FormPatch: values for the steps after it (recipients counted for a review step), which the form takes as it moves on. useWizard() takes the form's setData; rebuild a wizard form and run bezalel-core:stubs
    • A field in a repeater's rows can react (afterChange()): the form posts it by its row (segments.2.template_id), its reaction gets the row as a fourth argument and answers with the repeater's rows. fieldReactions() lists it as segments.template_id; run bezalel-core:stubs
    • Resource::$keepsDraft: a new record's page form kept in the browser until it's saved (lib/use-draft.ts), per person; a link with values starts fresh; "Start over" discards it. Compiled by both compilers
    • Layouts\Wizard and Step: a page form a step at a time — Back, Next (each step checked at {plural}/step-check, Resource::runStepCheck(): its fields' rules, then stepRules() and checkStep()), Save on the last; a save's mistake goes back to its step. components/forms/Wizard.tsx and lib/use-wizard.ts in the stubs. Compiled by both compilers
    • Resource::pageDirectory(): the folder its pages compile into, for a resource whose default (its name's plural) is already the app's own
    • Layout::visibleIf(Condition): a section of the form — a card and everything in it — shown only while its condition holds, as a field's is. Form only; compiled by both compilers
    • Resource::savedUrl(Model $record) and listUrl(): where a page form's save goes, and where the list is when it isn't the Index (which then redirects there). Field::editableFor($rule, onCreate: true) asks the lock on a new record's form too. Compiled by both compilers; only a resource that has them compiles differently
    • SelectField::createOption($fields, using:, label:, onEdit:): a picker's "+ New" — a small form in a dialog (components/forms/CreateOption.tsx), validated by its fields' rules, made through {plural}/field-create (Resource::runFieldCreate()), and selected. Compiled by both compilers; run bezalel-core:stubs
    • A reaction can offer choices: FormPatch::offer($title, $choices, dismissible:) opens a dialog (components/form-offers.tsx, mounted in app.tsx); a choice picked applies Field::afterPick()'s patch, one dismissed calls afterDismiss(). FormPatch::merge() sets several values. The generated fieldChange() passes pick/dismiss — rebuild a resource with a reacting field, and run bezalel-core:stubs
    • HiddenField: a value the form keeps but never shows or saves — what another field's visibleIf() reads, set by a reaction and worked out from the record (->value())
    • Resource::createFormData(Request $request): what a new record's page form opens with, worked out per request (a default from the app's settings, a value from the link) — sent to the create page as defaults, over the fields' build-time defaults. Compiled by both compilers; only a resource that has it compiles differently
    • A FileField in a relationship repeater's rows: attached to the row's own record after it's saved (Repeater::rowAttachments()), read back with the row; a new row starts it as null. A repeater in the rows gets a row type of its own in types.ts (OrderLegsStopsRow)
    • A Repeater in a repeater's rows — a leg's stops: a relation of each row synced under it (Repeater::syncRows()), or an array column of the row; its fields bound to the inner row (item2, update2), errors naming both rows, rules legs.*.stops.*.place, an async select searched as legs.stops.place_id. Compiled by both compilers; every other form compiles as before
    • BooleanField::toggleable(using:, when:): a yes/no cell that switches on a click and saves at once, offered per row to whoever may update the record (and never where updates wait for an approval), checked again by POST {plural}/{id}/toggle/{field} (ToggleController). Compiled by both compilers; BooleanCell takes toggle, CellExtras draws a link beside a switch. Rebuild a page to draw it
    • php artisan bezalel-core:stubs publishes the React and CSS stubs and, on every later run, brings them up to the package's without losing the app's changes: a file the app never touched is updated, one only the app changed is kept, one both changed is merged (with conflict markers where they changed the same lines), and a file marked @bezalel-keep is never touched. It keeps what it published in .bezalel/stubs — commit it. --dry-run shows what would happen; --base= merges an app published before against the stubs it came from
    • The shell's spacing as tokens an app sets in its app.css: --page-gutter, --card-padding, --dialog-padding, --control-height, --header-height, --table-cell-padding-y, --table-head-height and --nav-item-padding-y (as p-page, p-card, h-control, …)
    • Panel::breadcrumbs(false) hides the pages' breadcrumbs
    • <AppLayout withoutSectionSidebar> gives a wide page the room of a sections panel's sidebar, its pages moving to the strip under the header
    • components/app-header-actions.tsx, shipped empty, for the app's own buttons in the header
    • Resource::prepareRows() — called once with a table page's records before their columns are formatted, to load what a format() needs in one query instead of one per row
    • Page dialogs: <DialogLink href> opens a page — a record's view, edit or money — in a dialog over the list, as a modal would, and stays a plain link for a new tab. Links inside the dialog open there; a save from it reloads the list underneath and shows the page the save led to in the dialog (PageDialogRedirects); "Open as a page" leaves for the page itself
    • components/ui/skeleton.tsx — a pulsing placeholder the shape of what's still loading, for Inertia::defer()ed props
    • NavItem::badge() — a count beside a menu item, in the header, the More menu, the sidebar and the phone menu
    • Panel::headerRows(2) — a sections panel's top-level items and its sections in two header rows, so neither pushes the other into More
    • Panel::sectionSidebar(false) — a sections panel's pages in the strip under the header at every width, instead of a sidebar beside the page
    • NavSection::sidebar() — one section keeps its sidebar (or drops it) whatever the panel says
    • A sections panel's strip under the header no longer runs off the edge: what doesn't fit goes into More, as in the header
    • bodyClassName on SheetContent and DialogContent, for a panel that lays itself out edge to edge (p-0); className never reached the padded body
    • A selected colour (bg-selected, text-selected-foreground, a tint of the accent) and a selected Button variant, for the chosen tab, section, filter or page
    • Panel::transliterate(): a search term's other ways of writing (Cyrillic ↔ Latin, as the app's alphabet says); every async picker and every table search, of a query or of rows, also matches them (SearchTerms); tables: false keeps them to the pickers
    • LocationField and PlaceProvider: a place searched through the app's provider (a geocoder, or its own table), stored as its id; fills() copies the chosen place's coordinates or name into other fields of the form
    • Fieldset: one related record's fields edited inline with the parent's form — a belongsTo, morphTo, hasOne or morphOne, or a JSON column — and, with ->morph('type_field', [...]), one of several kinds (each its own model and fields) following another field of the form. Compiled by both compilers; on Show its fields are entries
    • Field::summarize(): a totals row under the table — sum, average, count, min or max of a column over every row the filters and search keep (one SQL aggregate, or over a ListsRows list's rows), per value of another column with by: (money per currency). DataTableSummary and TableFooter in the stubs; rebuild a page to draw it
    • MoneyField: an amount shown with its currency in the table and on Show — a fixed one or the record's own (currencyFrom('currency.name'), eager-loaded) — in the app's locale, rounded half up; MoneyField::formatUsing() for an app's own way of writing money
    • SelectField::dependsOn('carrier_id'): an async select whose options follow other fields — their values reach the query as a third argument (state), and changing one clears the choice; in a repeater's rows, the row's own fields
    • DisplayField: a value the form shows but never saves — worked out from the record (->value(fn (?Model $record) => …)) and updated by another field's reaction as the form changes
    • Fields locked by the record: Field::readonlyOnEdit() (set when the record is made), editableFor(fn ($record) => bool) (a price locked once signed) and disabled() — shown read-only, ignored on save. Resource::fieldAccess($record) works per record; an overlay form gets its record's access with the edit data (_fieldAccess). Rebuild an overlay resource with access rules to pick it up
    • DateFilter (on / on or before / on or after / between, with presets for today, this week, this month and last month) and NumberFilter (equals / at least / at most / between); Filter::valueOf() says what a filter means without a query — a ListsRows resource's rows() now gets a yes/no filter as a bool and a range as its ends
    • ListsRows: a Resource whose list is rows worked out in PHP (a report) rather than a query — rows($filters, $search), searched, sorted and paged in memory (DataTable::fromRows()), with rowMatches(), sortRows() and filterValues() to override
    • An async SelectField inside a Repeater's rows: its combobox searches as repeater.field (lines.product_id), which the field-search endpoint now finds. Rebuild a resource that has one
    • The licensed compiler: php artisan bezalel-core:compiler install downloads bezalel-compile for the machine (Linux or macOS, Intel or ARM) with the app's Bezalel Core licence — the same one Composer installs bezalel/core with, as its password for packages.bezalel.rocks — runs it only once Bezalel's signature and its checksum match, and gives it the licence; bezalel-core:build then compiles with it, and the app's ID is kept in bezalel.json (commit it). An app's post-install-cmd/post-update-cmd run bezalel-core:compiler install --optional. bezalel-core:compiler status says which compiler builds and the licence's state. See docs/guide/compiler.md
    • bezalel-core:build --check: writes nothing, and fails naming every compiled file that differs from a fresh build — for CI
    • Live lists and record pages, with realtime on: a list reloads when one of its records is saved, deleted or restored, tinting the rows someone else changed (Resource::live(), liveModels() for a list over a view or several tables); a record's page reloads when someone else changes it, or asks first if this person has typed (LiveRecord::share() for an app's own page); and who's here — the avatars of whoever has the record open, a pencil on whoever is typing, on the page and on its row (usePresence() for an app's own). See docs/guide/realtime.md. Run bezalel-core:stubs
    • Panel::analytics(): a page that groups, splits, totals, filters, charts and forecasts the rows each person may see, worked out in their browser (DuckDB in memory, optional: BEZALEL_ANALYTICS); reports saved under a name and pinned to a dashboard; and, with BEZALEL_ANALYTICS_AI, a question in words answered with a report or a checked, read-only query. See docs/guide/analytics.md. Run bezalel-core:stubs
    • Panel::globalSearch(): a search in the header (⌘K, Ctrl+K, /) over every Resource that sets $globallySearchable and the person may list, and the records they opened lately; Resource::globalSearchQuery() for a search of its own. Panel::listViews(): a list's search, filters, sort and view saved under a name, for oneself or everyone. Run bezalel-core:stubs
    • Panel::appLauncher(): a sections panel's menu in one row — every section as a tile with its first pages and a search over every page, the open section's pages as tabs. Run bezalel-core:stubs
    • Keyboard shortcuts: NavItem::shortcut()/NavSection::shortcut() — «g» then its key goes there; «?» lists them. Run bezalel-core:stubs
    • Panel::utilityBar(): tools along the bottom of every page whose windows stay open across pages — «Notes» (each person's own) and «Recently viewed», and the app's own in resources/js/utilities/. Run bezalel-core:stubs
    • RecordPath, RecordHighlights and RecordHoverCard: a record's stages with the action that moves it on, its key fields pinned on top, and its key fields on hover over anything that names it. Run bezalel-core:stubs
    • StatsWidget and Stat: a row of figures — a line under the value, its change against before, a tone and a small trend line — with no component of the app's. CalendarWidget and CalendarEvent: a month of events from several tables at once, each opening its address or an overlay, with actions. Run bezalel-core:stubs
    • Dashboard pieces: BarList (a ranked bar per figure), Progress, ProgressRing and Delta (a change coloured by what it means), on shared status tones (lib/tones). Run bezalel-core:stubs
    • BulkAction::allMatching(): «Select all N» runs a bulk action on every row the table matches, 500 at a time, queued past Resource::$bulkInBackgroundAbove
    • Resource::$pollSeconds: a table that reloads itself while it's looked at, for figures that move with no event to hear
    • Field::opens('lead-card', fn ($row) => [...]): a cell that opens the app's own overlay (useOverlayOpener(), openOverlay()). Run bezalel-core:stubs
    • Resource::rowTone(): a row tinted by its record's state. Field::sticky(): a wide table keeps its first column and header in view
    • Small field options: NumberField::prefix()/suffix() (a unit beside the input and in the table), RadioField::segmented(), ColorField, TextField::revealable() (a password with an eye)
    • Repeater::addLabel() for its add-a-row button, and its help() line drawn; Field::audio(): a recording played in its row
    • Filters on one line: Filter::asSwitch() (a small button group), Resource::$inlineFilters and Page::$inlineFilters (past that many, one «Filters» button and chips); Filter::asTabs(all: false); a fixed-list SelectFilter::multiple() picked from checkboxes; DateFilter::range(); DataTable::searchPlaceholder()
    • A list opens as this person last left it on this browser: its sort, and within twenty minutes its search and filters, unless the link says otherwise
    • A long list's export is made in the background past bezalel-core.exports.background_after rows and downloads by itself when ready
    • Panel::brandNameShown(false): the logo alone in the sidebar and header. Panel::passwordReset(false): no password reset, for a panel whose people get their passwords another way
    • UserResource::avatarFieldUsing(): an app's own photo field for its users
    • RecordDocuments: a record's documents opened from a page of the app's own
    Changed
    • **Breaking:** bezalel-core:build compiles only with the licensed compiler — install it with php artisan bezalel-core:compiler install (it needs a Bezalel Core licence; see docs/guide/compiler.md). A release of bezalel/core no longer carries the PHP compiler, so there is nothing to fall back to: without the compiler, the build stops and says how to install it. Apps keep running and deploying as they are — their compiled pages are committed
    • Placeholders while things load: a table's rows, cards and tab counts while a new look loads, the next page on scroll, and every panel, sheet and dialog while its content comes. Run bezalel-core:stubs
    • A bar along the top while the page waits on the server, for every fetch; core's own screens use the DatePicker for every date and time, in the page's language (month and weekday names, Monday first in Russian). Run bezalel-core:stubs
    • «Forgot your password?» says the same whether or not the account exists
    • A Resource's save is one transaction: the record and what its fields save after it (a repeater's rows, a fieldset's record, an upload's attachment), so one that fails leaves no half-saved record
    • Update the stubs with bezalel-core:stubs instead of vendor:publish --tag=bezalel-core-stubs --force, which overwrote the app's changes
    • A table's sort, search, filter or page reloads the widgets above it too (only: ['table', 'widgets']), so a widget that sums what the table lists follows its filters; update the stubs with bezalel-core:stubs
    • Generated pages give a card without a title its top padding as pt-card (the card token) instead of pt-4 sm:pt-6; rebuild with bezalel-core:build --force only if you want existing pages to follow it
    Fixed
    • A Resource listing rows (ListsRows) opens on its filters' defaults, hides the filters a person isn't offered, and counts its tab filters — over its rows by each row's value, or by its own filterCounts() (deferred with $defersTabCounts)
    • Two Resources with a card, list, action or widget component of the same name (OrderLists/Cards/OrderCard.tsx, TrackedOrders/Cards/OrderCard.tsx) drew the same one — whichever was found last. Each is now found in its own pages directory first (lib/component-registry.ts, DataTable::componentScope(), scope on record actions and widgets). Run bezalel-core:stubs
    • A Resource under a section (routeSlug() orders/orders) had its pages, controller and row actions ask for routes by its slug (route('orders/orders.index')), which no route is called — the page threw in the browser. Resource::routeName() (orders.orders) names them, in both compilers. Rebuild such a Resource with --force
    • A page with a deferred widget (Widget::defer()) went blank once its data came: Inertia's client put the widget alone in place of the list. With one deferred, widgets is keyed by widget (Widget::asProp()) and WidgetGrid takes either. Run bezalel-core:stubs
    • A Resource under a section (routeSlug() operations/leads) has its index, show, update and destroy routes named by its whole address (operations.leads.index), as its other routes are; they were named by the last segment (leads.index)
    • A slow page no longer leaves the old one sitting there as if nothing happened: past a moment the page dims under "Loading…", and the progress bar takes the accent colour so it shows on the canvas
    • A class passed to a component replaces its spacing token again (h-auto over h-control, p-0 over p-card): cn() didn't know the token names and kept both, so a wrapping tab bar overflowed its box
    • In light mode the open section, tab, page, settings page and filter now stand out: they were a grey a step off the page's own, the same as hover
    • A searchable select (AsyncCombobox) is now as tall as the inputs and selects beside it
    • A collapsed sidebar no longer opens and shuts again on every page change
    • A sections panel's header no longer scrolls sideways: the sections that don't fit move into the More menu, as a header menu's pages do
    • A new app's page no longer stays white under npm run dev (its layout lacked @viteReactRefresh)
    • A page error shows a message and a reload instead of a white window, and a page left open across a new build reloads onto it
    • Picking an option in a select inside a popover no longer closes the popover
    • A toolbar's icon-only action is the height of the buttons beside it
    • A filter left at its default isn't counted as applied, and its chip reads «Status: Live»
    • Tabs wrap instead of running off the page; a widget that draws nothing takes no room
    • History: a record in the bin opens its history; a field the model hides is never shown; a list or object reads as JSON; an app's own activity model is used
    • A repeater keeps a row's fields that have no rules of their own
    • Downloads and imports say they're working, and a second click doesn't start another; a handled action's file reaches the browser
    • The sign-in throttle's message is shown under the panel's own field; a sign-in link works while someone is signed in
    • The comments sheet says why a comment was refused
    • bezalel-core:export writes its JSON untouched when an AI agent runs it
  2. Starter

    1.3.0

    Made from bezalel/core 0.4.0.

    Changed
    • **Breaking:** a new app is installed with your Bezalel Core licence — composer config --global http-basic.packages.bezalel.rocks licence <your licence> — and bezalel-core:build compiles with the licensed compiler, which composer install and composer update put beside the app (bezalel-core:compiler install). The app's first build writes bezalel.json, its ID to the licence: commit it
    • Everything bezalel/core 0.4.0 brings, its pages and components already in place: live lists and who's here, analytics, the header's search and saved list views, the app launcher, keyboard shortcuts, the utility bar, record paths and hover cards, stats and calendar widgets, wizards and drafts, and the rest of its changelog
  3. Forge

    1.10.0

    Keeping what the panel installed up to date: fixes for PHP and PostgreSQL arrive overnight, and the rest is one click away.

    Added
    • **Updates waiting**: the panel checks every few hours for newer versions of what it installed — nginx, PHP, MySQL, PostgreSQL, Node.js and the extras — and says so in Needs attention and on the Software page, with Upgrade now
    Changed
    • Automatic security updates now also apply updates to PHP and PostgreSQL from the repositories the panel installs them from, not only Ubuntu's own packages. A server where they were already on shows Include them on the Security page
    Fixed
    • Zipping files on the Files page no longer fails with "zip: command not found" on a server that never had zip installed
  4. Forge

    1.9.0

    phpMyAdmin takes real database imports, and its limit is yours to set.

    Added
    • **phpMyAdmin's largest import can be changed**: the Databases page says how large an import phpMyAdmin and phpPgAdmin take, and an administrator can set it up to 1 GB. New servers start at 256 MB instead of PHP's 2 MB; no site's own limits change
    Fixed
    • Importing into phpMyAdmin or phpPgAdmin no longer fails with a 502 for anything larger than 64 KB: they now accept as much as their own limit allows
    • Removing a site now also clears the git-hosting tasks that failed for it (creating its repository, merging into it) from Needs attention, as it already did for its other tasks
  5. Forge

    1.8.0

    Easier for everyone: bring a whole site and its database over in one go, see at a glance what needs fixing on a site, undo a deletion, and read the guides in Russian and German.

    Added
    • **Import a database**: on the Databases page, a database's menu has Import a dump… — a .sql or .sql.gz export, up to 1 GB. It loads as a login that reaches that one database alone, so an export can only change what is in it, and one from another server just works: its old database name, and the accounts its views name, are taken care of
    • **A zip can bring its database**: a .sql export beside the site's files is imported into the database made in the same dialog, and a WordPress site's address is changed to the new one, so a moved WordPress site opens at its new address straight away
    • **HTTPS from the start**: Add a site gets HTTPS as part of creating the site — with a certificate already on the server that covers it, or from Let's Encrypt once the domain points here. The email for expiry warnings is remembered, and filled in next time
    • **Does the domain point here?** Add a site, the HTTPS tab and Move to another domain say, as you type, whether a domain points to this server — and if not, the exact record to add at your DNS provider
    • **A health check on every site**: its Overview says whether the domain points here, which backups hold it — with Add to a backup when none does — whether its PHP version still gets security fixes, and how many errors its logs show for the last day
    • **A recycle bin** on the Files page: Delete moves things there for a week, and Restore puts them back where they were
    • **A first-run walkthrough**: the Server page's checklist now also covers where alerts go and turning on the firewall, links each step to its guide, and keeps the step in view on the page it is done on
    • Queue workers can be changed — the queue, the processes, and how often and how long a job is tried — not only removed
    • **The guides in Russian and German**, and a ? beside the settings that are not obvious, opening the right part of a guide
    Changed
    • An ordinary deploy now restarts the site's queue workers, as a release-based one always did, so they run the new code
    • The Scheduler tab is on every site: a site's own cron jobs work whatever it is, not only on Laravel
    • Downloading one file gives you the file, not a zip of it
    • A site's Settings tab folds the settings most sites never change — extra proxy paths, never-cached routes, rate and connection limits, raw nginx directives — under More settings, which opens by itself when one is in use
    • The HTTPS tab no longer offers the certificate a site is already using
    • The dashboard's scrollbars are thin and in its colours on every system, and on a Mac they no longer cover the last row of a table
    • The More panel fits its page names instead of scrolling sideways, and the backup and application dialogs scroll once rather than twice
  6. Forge

    1.7.0

    Setting up a site from a zip of its files, moving a site to another domain, and guides to the whole panel.

    Added
    • **A site from a zip file**: under Add a site → Start with, upload the site's files as a .zip (up to 1 GB). The panel reads it to fill in the form — Laravel, Symfony, WordPress, plain PHP or static, and the PHP version — strips the one folder a zipped folder has, and unpacks it once the site exists
    • A WordPress zip gets its wp-config.php moved out of the folder the site serves and pointed at a database made in the same dialog; any other application gets the database in its .env, and the commands you list (composer install, migrations) run once the files are in place
    • **Move a site to another domain**, from its Settings tab: its folder, configuration, logs, deploy settings, scheduler, workers, Node.js application and backup policies all follow, in one step that is undone if anything is refused. The old domain redirects to the new one, keeps serving the site too, or goes
    • A site on HTTPS keeps its certificate through a move, and is never redirected to a name its certificate does not cover
    • A preferred-hostname choice on the Settings tab that redirects every other name to the site's own domain
    • **Guides**: how to use the panel, task by task — adding a site, HTTPS, moving a site, deploying, databases, files, backups, security and the rest. The **?** at the top of every page opens the guide for the page you are on, and every guide is in the ⌘K search. English for now; Russian and German to follow
    • **Get a new certificate** on the HTTPS tab of a site already on HTTPS — after adding a name, moving it, or to replace a staging certificate — and the site stays on HTTPS while it is asked for
    Changed
    • Sites, deploy settings and backup policies are saved in a newer format. A panel rolled back to 1.6.0 still shows them but refuses to change them; upgrade again to change them
    Fixed
    • A site serving a reused or wildcard certificate no longer drops to plain HTTP the next time its settings are saved, and a site with HTTPS turned off no longer turns it back on at its next change
    • Asking for a certificate no longer takes a site that is already on HTTPS back to plain HTTP while it waits — or for good, when Let's Encrypt says no
    • Uploading on the Files page answers in your language rather than English
    • Out-of-date help text: the Servers page said managing another server was still to come, the Cron and Supervisor pages sent you to a site's Settings tab for what is on its Scheduler tab, and the Reverb switch suggested running composer from the Commands tab
  7. Forge

    1.6.0

    The panel in English, Russian and German.

    Languages
    • The panel speaks English, Russian and German. Choose yours from the language button in the header or on the sign-in page; it is saved to your account, so it follows you to every computer you sign in from
    • Every page of the dashboard is translated, and so is what the server says: errors under a form, what needs attention, tasks — their titles, every step and their errors — and a site's activity history
    • Notifications and emails go out in each channel's owner's language; a server-wide channel in the server's
    • A pull request's review is posted in the site owner's language; the AI's own findings stay as the model wrote them
    • An AI app's suggestions are shown in your language on the AI access page; the AI itself still reads them in English
    • Dates, numbers and sizes are written the way your language writes them
    • An administrator sets the server's own language on the Account page, for the server-wide notification channels and for a browser that asks for a language the panel does not speak
    • Scripts using the API get English as before; send Accept-Language: ru or de for a message in that language, and match on an error's code, which is never translated
    Fixed
    • Counts read properly in English too — "1 advisory", "1 repository", "1 earlier line" rather than "1 advisories" — and a PostgreSQL extension error no longer says "could not reading"
  8. Forge

    1.5.0

    Git hosting on your own server: each site's code in a repository here, every change reviewed before it deploys — or pushed straight to the site, if you work alone.

    Git hosting on this server
    • Install git hosting (Forgejo) from the Git page: your own git server at its own address, with a web interface, so a site's code never has to live on GitHub
    • Each person sets up their own git access and adds their computer's SSH key on the Git page, with how to make one
    • A site's Deploy tab gives it a repository: empty, for code already on your computer (one push puts it there and deploys it), with a README to start from, or with the site's current files after a preview that refuses secrets
    • Move a site's repository from GitHub, GitLab or Bitbucket here in one step, with every branch, tag and commit; the deploy key and webhook the panel had added there are removed
    • Plain git all the way: git push -u origin my-change opens the pull request for you, and the site deploys when you press **Merge and deploy** — exactly the commit you saw. Pushing straight to the deployed branch is refused
    • Each site's Deploy tab shows how to get its code, change it and deploy it, with your own addresses and copy buttons, and the documentation has a step-by-step guide
    • Working alone? Turn **Review before deploy** off and git push to main deploys straight away; branches still open reviewed pull requests when you want a second look. Only a signed-in person can turn it off, never an API token or an AI suggestion
    • Every pull request is reviewed before you approve it: committed secrets, leaked keys and vulnerable packages always, and an AI review against your framework's rules and the repository's own REVIEW.md — on the Deploy tab and inline on the pull request, with a notification when it is ready, and a switch per site to keep its code away from the AI
    • A site's own tests and linters run against every pull request, in a confined account that owns nothing and can reach only the package registries; a failing test is in the review with its output
    • The review only reports: nothing deploys until a person approves it, and a change that tries to talk the reviewer into approving is reported as a finding
    • Back up repositories, or all of git hosting, to S3, Google Drive or a local folder; the dashboard warns about any repository no scheduled backup covers
    • The dashboard warns when a protected branch was loosened in Forgejo, with a button to put the rule back, and failed sign-ins to git hosting are banned under the server's ban policy
    • Update git hosting from the Git page when a panel release carries a newer Forgejo: a dump is taken first, and a version that does not start is put back
    • AI assistants connected over MCP can read a site's pull requests and their reviews, and cannot merge or close them
    • Moving to another server carries git hosting: every repository, and its database at the switch-over
    Fixed
    • Deploys now check GitHub's, GitLab's and Bitbucket's SSH host keys against the pinned ones. A site's own account could not read the file holding them, so until now a server's first connection to each trusted whatever key it was shown
  9. Forge

    1.4.1

    Everything Bezalel Forge does, in one place. Until the official release this one entry is kept up to date rather than a list of small versions; 1.0.0 to 1.4.0 were the pre-releases that built it.

    Websites
    • PHP, Laravel, Statamic, WordPress, Node.js and static sites behind nginx (or Apache), each running as its own Linux account
    • Change a website's other hostnames at any time on its Settings tab, with a reminder to get the certificate again when an HTTPS site gains a name
    • A new site straight from a GitHub, GitLab or Bitbucket repository, with its deploy key and webhook added for you
    • Release-based deploys that switch in only when every command has passed, one-click rollback, push-to-deploy, and the log of every step
    • Queue workers, the scheduler, supervisor programs and cron jobs per account; Node.js apps kept running by pm2
    • Private Composer packages per site, through registry credentials and repository overrides
    • Let's Encrypt certificates for every site, wildcards through Cloudflare DNS, and a warning long before one expires
    Databases
    • MySQL and PostgreSQL databases and users per account
    • Click a database to open it in phpMyAdmin or phpPgAdmin already signed in: a login for that database alone, gone after an hour, and no password typed or shown
    Moving to Forge
    • Move from ISPmanager, cPanel, Plesk or any nginx server over SSH: a scan lists websites, databases, pm2 applications, supervisor programs, systemd services, containers and Compose stacks — in tabs, kept until you scan again — and you pick what to bring and the account it goes to
    • Compare settings before a move: what was changed from the defaults in the old server's PHP (every version, ISPmanager's own builds, each account's and each site's own), MySQL or MariaDB, and PostgreSQL, beside how this server is set — what differs, what was sized for the old server's memory, what this server's version no longer has — without changing either server
    • Forge creates the websites and databases and brings everything over in one task: each database gets its own user and a generated password written into its site; workers and services move with their site, with its new folder and this server's PHP; pm2 applications start in the new account's home; containers keep their image, volumes and settings; Compose stacks land where the Containers page manages them
    • See and change exactly what is copied before anything is made — every folder, from where to where, with its size — copy a parent or a subfolder instead, skip node_modules, vendor, logs or any pattern, add folders, and follow warnings for what the plan probably misses; symlinks into moved folders are repointed
    • A test address for every moved website, such as shop-example-com.<test domain>, over HTTPS at once with the server's wildcard certificate, to check it before switching DNS
    • Run a move again with Replace to overwrite what an earlier move made with the old server's current files and data
    • Move a whole Forge server to a new one — sites, databases, certificates and settings — with a short switch-over
    Security
    • A web application firewall per site — the OWASP Core Rule Set, watched in Detect, then switched to Block — with exclusions and a ban jail
    • A firewall page (ufw) that opens SSH, HTTP and HTTPS first and undoes itself unless you confirm, fail2ban bans you can see and lift, and hardening checks
    • Two-factor sign-in with authenticator codes, recovery codes and passkeys; personal API tokens
    • Administrators can look at the panel as another account (Users → Sign in as), with a banner and Back to my account; that account's password, two-factor sign-in and tokens stay out of reach, and it ends by itself after an hour
    Running the server
    • Backups on a schedule to S3-compatible storage, Google Drive (connected with a code, nothing to set up in Google Cloud) or the server itself, and restores that keep what they replace
    • Files and a terminal in the browser, with a code editor, uploads and downloads
    • Docker images, containers and Compose stacks, routed to a domain
    • Several servers managed from one panel
    • Notifications by email, Slack, Discord, Telegram or a webhook, and a "needs attention" view of what is wrong; the Tasks badge forgets failures you have already seen
    • Support from the panel (Get help), and error reports to Bezalel — off by default, with secrets removed and shown to you before anything is sent
    AI
    • Claude and other AI over MCP (OAuth 2.1): they read the server and propose changes, which wait for a person to approve
    • An assistant inside the panel, with a key the administrator brings
    • Sandboxes for building Bezalel apps with your own AI, each in its own confined account with a private preview
    Updates and licence
    • Signed releases: the panel updates itself from its own page, only with what Bezalel's release key signed, and a thin banner says when a new version is out
    • An installer that checks every file before anything runs, and a licence from your bezalel.rocks account that checks in — renewals and revocations arrive by themselves, your account shows each server's version and site count, and the licence's website limit applies
  10. Forge

    1.4.0

    Everything Bezalel Forge does, in one place. Until the official release this one entry is kept up to date rather than a list of small versions; 1.0.0 to 1.3.0 were the pre-releases that built it.

    Websites
    • PHP, Laravel, Statamic, WordPress, Node.js and static sites behind nginx (or Apache), each running as its own Linux account
    • Change a website's other hostnames at any time on its Settings tab, with a reminder to get the certificate again when an HTTPS site gains a name
    • A new site straight from a GitHub, GitLab or Bitbucket repository, with its deploy key and webhook added for you
    • Release-based deploys that switch in only when every command has passed, one-click rollback, push-to-deploy, and the log of every step
    • Queue workers, the scheduler, supervisor programs and cron jobs per account; Node.js apps kept running by pm2
    • Private Composer packages per site, through registry credentials and repository overrides
    • Let's Encrypt certificates for every site, wildcards through Cloudflare DNS, and a warning long before one expires
    Databases
    • MySQL and PostgreSQL databases and users per account
    • Click a database to open it in phpMyAdmin or phpPgAdmin already signed in: a login for that database alone, gone after an hour, and no password typed or shown
    Moving to Forge
    • Move from ISPmanager, cPanel, Plesk or any nginx server over SSH: a scan lists websites, databases, pm2 applications, supervisor programs, systemd services, containers and Compose stacks — in tabs, kept until you scan again — and you pick what to bring and the account it goes to
    • Compare settings before a move: what was changed from the defaults in the old server's PHP (every version, ISPmanager's own builds, each account's and each site's own), MySQL or MariaDB, and PostgreSQL, beside how this server is set — what differs, what was sized for the old server's memory, what this server's version no longer has — without changing either server
    • Forge creates the websites and databases and brings everything over in one task: each database gets its own user and a generated password written into its site; workers and services move with their site, with its new folder and this server's PHP; pm2 applications start in the new account's home; containers keep their image, volumes and settings; Compose stacks land where the Containers page manages them
    • See and change exactly what is copied before anything is made — every folder, from where to where, with its size — copy a parent or a subfolder instead, skip node_modules, vendor, logs or any pattern, add folders, and follow warnings for what the plan probably misses; symlinks into moved folders are repointed
    • A test address for every moved website, such as shop-example-com.<test domain>, over HTTPS at once with the server's wildcard certificate, to check it before switching DNS
    • Run a move again with Replace to overwrite what an earlier move made with the old server's current files and data
    • Move a whole Forge server to a new one — sites, databases, certificates and settings — with a short switch-over
    Security
    • A web application firewall per site — the OWASP Core Rule Set, watched in Detect, then switched to Block — with exclusions and a ban jail
    • A firewall page (ufw) that opens SSH, HTTP and HTTPS first and undoes itself unless you confirm, fail2ban bans you can see and lift, and hardening checks
    • Two-factor sign-in with authenticator codes, recovery codes and passkeys; personal API tokens
    • Administrators can look at the panel as another account (Users → Sign in as), with a banner and Back to my account; that account's password, two-factor sign-in and tokens stay out of reach, and it ends by itself after an hour
    Running the server
    • Backups on a schedule to S3-compatible storage, Google Drive (connected with a code, nothing to set up in Google Cloud) or the server itself, and restores that keep what they replace
    • Files and a terminal in the browser, with a code editor, uploads and downloads
    • Docker images, containers and Compose stacks, routed to a domain
    • Several servers managed from one panel
    • Notifications by email, Slack, Discord, Telegram or a webhook, and a "needs attention" view of what is wrong; the Tasks badge forgets failures you have already seen
    • Support from the panel (Get help), and error reports to Bezalel — off by default, with secrets removed and shown to you before anything is sent
    AI
    • Claude and other AI over MCP (OAuth 2.1): they read the server and propose changes, which wait for a person to approve
    • An assistant inside the panel, with a key the administrator brings
    • Sandboxes for building Bezalel apps with your own AI, each in its own confined account with a private preview
    Updates and licence
    • Signed releases: the panel updates itself from its own page, only with what Bezalel's release key signed, and a thin banner says when a new version is out
    • An installer that checks every file before anything runs, and a licence from your bezalel.rocks account that checks in — renewals and revocations arrive by themselves, your account shows each server's version and site count, and the licence's website limit applies
  11. Core

    0.3.0

    Everything bezalel/core does, in one place. Until the official release this one entry is kept up to date rather than a list of small versions; 0.1.0 to 0.2.0 were the pre-releases that built it.

    Look and feel
    • A calm, modern admin: an off-white page with white cards and tables on soft shadows, one accent colour (blue by default, with a black-and-white Neutral among the presets), the Geist font, rounded corners and numbers that line up in columns
    • A compact density that fits far more, with table rows of about 28px
    • Dialogs, sheets, menus and select lists that animate in and out, shorter when the system asks for reduced motion
    • Soft charts on dashboards and website pages: smooth lines, fading areas, rounded bars and a donut with its total in the middle
    • Header navigation that folds what doesn't fit into a **More** menu, and a proper phone menu for the header and sections layouts
    • Design rules every page and component follows, the same ones Claude reads when it builds UI in an app, and an EmptyState component
    Pages that explain themselves
    • Resource::$description puts a title and that sentence on the Index page; Field::help() adds a line under the form input and an explanation on the column header
    • Empty tables say why: nothing yet (with Create), or nothing matches the search and filters (with a button that clears them)
    • "How this page works" from a **?** in the header: a Resource's or Page's description() and guide(). With the assistant on, **Ask about this page** answers from what the page really has.
    • A getting-started checklist, Panel::gettingStarted([...]): Step classes that each decide from the app's data whether they're done (CreateFirstRecord ships)
    Resources and admin pages
    • One Resource class per entity with self-describing fields, compiled by bezalel-core:build into a real controller and React pages an app can open and edit
    • Fields for text, numbers, dates, selects (async and combobox), radios, booleans, tags, rich text, images, files, documents and repeaters, with reactive visibleIf/requiredIf and per-field access (Field::can(), canEdit())
    • Tables written out per Resource over a shared hook: sort, search, filters as tabs or menus, row, toolbar and bulk actions, a card view, column choice, related tables and cells with links, second lines, colours and copy
    • Forms in a page, dialog, sheet or drawer, laid out with tabs, grids, flex and cards; Show pages with record actions
    People and panels
    • Teams, roles and permissions, owner-scoped Resources, impersonation, comments on records and an activity log
    • Panels on their own paths or domains, with their own sign-in and sign-up, sidebar, header or sections navigation, density and role switcher
    • Languages through Panel::locales(), with Russian included
    • Notifications to an inbox, by push and email; an installable app; reminders and campaign lists written as classes
    Content
    • A media library with folders, and documents edited in the browser through OnlyOffice
    • A website builder (Puck): pages and menus in the browser, blocks in TSX, drafts, revisions, scheduled publishing, several languages, redirects, a sitemap and server rendering
    • Components built on a canvas (exportable as TSX), datasets and charts over an app's own records, and site bundles for a gallery
    • Forms authors build, with steps, signatures, signed PDFs, confirmation links and stored submissions — every rule derived in PHP
    Support desk
    • A support desk (Panel::support()): people ask for help from the user menu and follow their requests; agents answer from a Support inbox, with internal notes, assignment, priorities and statuses. Answers are emailed with the ticket's number, solved tickets close by themselves, and tickets are tools for the assistant and MCP — see docs/guide/support.md
    • The desk takes email (a mailbox read over IMAP, replies threaded onto their tickets) and tickets from other apps through its API, and Panel::helpFrom() lets an app's people ask the desk of the people who made it
    • With the assistant set up, every new ticket and requester reply gets a draft for agents only: the likely cause, how to check it, the fix and a suggested reply, used with one click
    AI
    • Resources, pages, dashboards and the site builder as MCP tools under each person's own permissions, an assistant in the page editor, connected apps and the AI activity page
    • Approval rules over Claude's calls, people's actions and saves, and website form submissions — approvers can answer by email
    Building with it
    • bin/new-app and the bezalel/starter kit; bezalel-core:request, login-link, tool, user and mcp-token for working with an app from the command line
    • Usage guides that ship with the package, in docs/guide/ — start with docs/guide/README.md; the package carries only what an app uses
    • An app that takes Core from git gets the branch it asks for
    Upgrading from 0.2
    • Run npm install @fontsource-variable/geist @fontsource-variable/geist-mono tw-animate-css and republish the stubs (php artisan vendor:publish --tag=bezalel-core-stubs --force). Then in resources/css/app.css, replace the colour tokens with @import './bezalel.css'; after @import 'tailwindcss';, keeping below it only the tokens you changed.
    • Run php artisan migrate, which adds the checklist's table. Rebuild a Resource with bezalel-core:build --force once you give it a description or help.
  12. Starter

    1.2.0

    Made from bezalel/core 0.3.0. Everything the starter gives a new app, in one place. Until the official release this one entry is kept up to date rather than a list of small versions; 1.0.0 to 1.1.0 were the pre-releases that built it.

    Added
    • A Laravel 13 app with bezalel/core set up: the admin panel with users, roles and the media library, and the website builder
    • Core's calm, modern look: the panel's theme comes from resources/css/bezalel.css, and resources/css/app.css holds only the app's own changes to it
    • The support desk's pages — the Support inbox, Help, and "Get help from …" — ready for Panel::support() and Panel::helpFrom() (see vendor/bezalel/core/docs/guide/support.md)
    • The panel's MCP server with Passport, so Claude can use the app under each person's permissions
    • composer create-project bezalel/starter from packages.bezalel.rocks, with bezalel/core installed as its release zip — a Composer token for packages.bezalel.rocks (Account → Composer access) is all it needs
    • A CLAUDE.md and AGENTS.md guide for building on Core with an AI, pointing at the guides shipped in vendor/bezalel/core/docs/guide/ and at the design rules every page and component follows
  13. Compiler

    0.2.0

    Everything bezalel-compile does, matching bezalel/core 0.3.0. Until the official release this one entry is kept up to date rather than a list of small versions; 0.1.0 was the pre-release that built it.

    Added
    • bezalel-compile: reads bezalel-core's descriptions of Resources and Pages on stdin and writes the generated controllers, Inertia pages, row types and component scaffolds as JSON — never the app's code, never to disk
    • Held byte for byte to bezalel-core's compiler contract (testdata/contract/)
    • Index tables written out per Resource, record types per Resource, Show pages with record actions, related tables and lists, and overlay forms
    • Field reactions, per-field access, table cell extras, and every generated word through t() and __()
    • Pages that explain themselves: a Resource's description opens its Index page with a title and that sentence, and a field's help goes under its form input and onto its column header
    • A Bezalel licence: app activation, phone-home and the signer
  14. Forge

    1.3.0

    Everything Bezalel Forge does, in one place. Until the official release this one entry is kept up to date rather than a list of small versions; 1.0.0 to 1.2.0 were the pre-releases that built it.

    Websites
    • PHP, Laravel, Statamic, WordPress, Node.js and static sites behind nginx (or Apache), each running as its own Linux account
    • A new site straight from a GitHub, GitLab or Bitbucket repository, with its deploy key and webhook added for you
    • Release-based deploys that switch in only when every command has passed, one-click rollback, push-to-deploy, and the log of every step
    • Queue workers, the scheduler, supervisor programs and cron jobs per account; Node.js apps kept running by pm2
    • Private Composer packages per site, through registry credentials and repository overrides
    • Let's Encrypt certificates for every site, wildcards through Cloudflare DNS, and a warning long before one expires
    Databases
    • MySQL and PostgreSQL databases and users per account
    • Click a database to open it in phpMyAdmin or phpPgAdmin already signed in: a login for that database alone, gone after an hour, and no password typed or shown
    Moving to Forge
    • Move from ISPmanager, cPanel, Plesk or any nginx server over SSH: a scan lists websites, databases, pm2 applications, supervisor programs, systemd services, containers and Compose stacks — in tabs, kept until you scan again — and you pick what to bring and the account it goes to
    • Forge creates the websites and databases and brings everything over in one task: each database gets its own user and a generated password written into its site; workers and services move with their site, with its new folder and this server's PHP; pm2 applications start in the new account's home; containers keep their image, volumes and settings; Compose stacks land where the Containers page manages them
    • See and change exactly what is copied before anything is made — every folder, from where to where, with its size — copy a parent or a subfolder instead, skip node_modules, vendor, logs or any pattern, add folders, and follow warnings for what the plan probably misses; symlinks into moved folders are repointed
    • A test address for every moved website, such as shop-example-com.<test domain>, over HTTPS at once with the server's wildcard certificate, to check it before switching DNS
    • Run a move again with Replace to overwrite what an earlier move made with the old server's current files and data
    • Move a whole Forge server to a new one — sites, databases, certificates and settings — with a short switch-over
    Security
    • A web application firewall per site — the OWASP Core Rule Set, watched in Detect, then switched to Block — with exclusions and a ban jail
    • A firewall page (ufw) that opens SSH, HTTP and HTTPS first and undoes itself unless you confirm, fail2ban bans you can see and lift, and hardening checks
    • Two-factor sign-in with authenticator codes, recovery codes and passkeys; personal API tokens
    • Administrators can look at the panel as another account (Users → Sign in as), with a banner and Back to my account; that account's password, two-factor sign-in and tokens stay out of reach, and it ends by itself after an hour
    Running the server
    • Backups on a schedule to S3-compatible storage, Google Drive (connected with a code, nothing to set up in Google Cloud) or the server itself, and restores that keep what they replace
    • Files and a terminal in the browser, with a code editor, uploads and downloads
    • Docker images, containers and Compose stacks, routed to a domain
    • Several servers managed from one panel
    • Notifications by email, Slack, Discord, Telegram or a webhook, and a "needs attention" view of what is wrong; the Tasks badge forgets failures you have already seen
    • Support from the panel (Get help), and error reports to Bezalel — off by default, with secrets removed and shown to you before anything is sent
    AI
    • Claude and other AI over MCP (OAuth 2.1): they read the server and propose changes, which wait for a person to approve
    • An assistant inside the panel, with a key the administrator brings
    • Sandboxes for building Bezalel apps with your own AI, each in its own confined account with a private preview
    Updates and licence
    • Signed releases: the panel updates itself from its own page, only with what Bezalel's release key signed, and a thin banner says when a new version is out
    • An installer that checks every file before anything runs, and a licence from your bezalel.rocks account that checks in — renewals and revocations arrive by themselves, your account shows each server's version and site count, and the licence's website limit applies
  15. bezalel.rocks

    1.1.1

    Everything bezalel.rocks does, in one place. Until the official release this one entry is kept up to date rather than a list of small versions; 1.0.0 and 1.1.0 were the pre-releases that built it.

    The website
    • Forge, Core and Ecclesia told in full, with drawn screens of each, light and dark, and a Get started form
    • A public changelog at /changelog, published from each product's CHANGELOG.md
    • A Legal text block and a legal menu at the foot of every page, for the privacy policy and terms of service
    Your account
    • One account for every product: licences, servers, apps and downloads
    • Licences for Forge and Core, signed by the portal, with app activation and check-in
    • Forge installed on your own server from your licence's page, with a key that is removed when the install ends
    • Support: ask for help from your account and follow the answers; support tokens let apps built on Core ask Bezalel for help from inside the app
    Packages and releases
    • packages.bezalel.rocks: the private Composer repository for bezalel/core and bezalel/starter, built from tags
    • Forge's release channel: signed releases imported as candidates, published to stable, served to licence holders
    For Bezalel's staff
    • A Support inbox, with Claude drafting a diagnosis and a reply to edit
    • Tickets from Forge arrive with the licence, the server and its recent errors; the errors panels share (when their administrator allows it) are grouped under Admin → Forge errors, where Claude can say where to look, and a failed deploy is marked as probably the customer's application. Reports are deleted after 30 days