LearningFlow.ai
Bases

Your class data, in tables everyone shares.

Like a spreadsheet, but every teacher sees the same one, and your apps and tools read from it.

  • Class lists
  • Attendance
  • Marks & records

A base for one class.

Three tables. Teachers, the attendance app and the quiz all use the same one.

6C · 4 rows
NameClassReading levelGuardian phoneNotes
Aisyah Rahman6CLevel 5+65 9xxx 1204Moves up a level next term
Ben Tan6CLevel 3+65 8xxx 7719Extra reading support, Tuesdays
Chloe Lim6CLevel 6+65 9xxx 0045
Dev Kumar6CLevel 4+65 9xxx 3381Allergic to peanuts
Who can open this base2 teachers · 1 attendance app · 1 quiz

Keys, not passwords.

Every app that touches the base holds its own key. Take one back and only that app stops.

Access keys for a base: one anchor key for a platform, three app keys under it, each with suspend and revoke

The end of the six spreadsheets.

One copy, everyone

Change a phone number once. Every teacher and every app sees it.

Your apps read from it

The attendance app, the quiz, the report tool: same table, no copies.

Hand out a key, take it back

Each person or app gets its own key. Revoke it and access stops, nothing else breaks.

Feels like a spreadsheet

Columns, rows, filters. Nothing new to learn.

Bases is a shared database. Making one creates a private PostgreSQL schema holding only that base's tables, and a separate record of its table names, field names and links. From there you add tables and fields, put rows in, read them back with filters and sorting, upload a CSV, download a CSV, and tie one table to another. Every read and every write, by a person or by an app, is checked against a grant called a contract first.

A base open: its tables as tabs, and the toolbar for hiding fields, filtering, grouping, sorting and row height, with Contracts and Tools in the corner Base, grid view
Hide fields, filter, group, sort and row height across the top; the number beside Contracts is how many grants reach this one base.
  • 108HTTP endpoints
  • 12Field types you can choose
  • 14Filter operators in the grid
  • 5Levels a grant passes down
  • 6Kinds of holder a grant names
  • 843Automated tests in the code

All of it, not the highlights.

What a base is made of

A base is a database of its own, not a folder inside a shared one.

  • Its own private schemaMaking a base creates a PostgreSQL schema named base_ plus a 24-character id. One base's tables cannot be seen from inside another.
  • Structure and rows kept apartBase names, table names, field names and types are held in MongoDB. The rows themselves are held in PostgreSQL.
  • Twelve field typesTEXT, VARCHAR, INTEGER, BIGINT, NUMERIC, BOOLEAN, DATE, TIMESTAMP, TIMESTAMPTZ, JSONB, LINK and RELATION. Ten real types once LINK and RELATION are collapsed.
  • Four columns you always getEvery table is created with id, created_at, updated_at and deleted_at. A field using one of those names is refused, and a CSV import drops that column.
  • A field can be made uniqueMarked unique, it becomes a real database constraint when the table is created, not a check the interface performs.
  • What else a field carriesA description, a position, a default value, a required flag, a primary flag, and a free-form settings object the front end can use.
  • Change the table afterwardsFields can be added and renamed after rows exist, and adding one runs a real ALTER TABLE. An unknown type is refused with every accepted type listed back.
  • Order, icon and descriptionThe order tables appear in can be saved, and each table carries an optional icon and a description.

Joining one table to another

Two tables can be tied together, and the tie is checked on every write.

  • Two shapes of linkOne-to-many and many-to-many. Both are set up through the API rather than by writing SQL.
  • One-to-many adds a columnAn INTEGER column on the first table holding the id of a row in the second, named after the target table unless you name it yourself.
  • Many-to-many builds a tableA join table is created for you, with a rule stopping the same pair being recorded twice, and it is registered as a real table in the base.
  • The join table behaves normallyIt gets a collision-proof name, the two table names plus an eight-character suffix, and its own id, created_at and updated_at columns.
  • A link pointing nowhere is refusedWriting a link value for a row that does not exist is rejected before the row is saved.
  • The service checks, not PostgreSQLNo FOREIGN KEY constraint is ever created. The check happens in the application, before the write.
  • Deleting a parent takes the childrenChild rows that pointed at it are deleted and the matching join-table entries cleared. This one cascade is permanent: those children do not reach the recycle bin.
  • Ask what is linked to whatOne call lists a table's links, another returns the related rows of a single record with paging. Removing a link drops the join table and the column.

Reading a table

Filtering, sorting and grouping are query parameters, so any app can ask what the grid on screen asks.

  • Fourteen filter operatorseq, neq, gt, lt, gte, lte, like, nlike, in, nin, is, isnot, blank and notblank.
  • Conditions joined, values boundConditions combine with and / or, and every value is handed to PostgreSQL as a bound parameter rather than pasted into the SQL text.
  • Sort on several fieldsA leading minus sign means descending. A field name that does not exist is ignored rather than failing the whole request.
  • Pick fields, group rowsAsk for only the columns you need, and group by one column. Each group comes back with a count and up to 100 rows.
  • Page sizes, statedThe grid returns 25 rows by default and at most 200; limit=0 returns everything matching. The plain row list does 50 by default and at most 1,000.
  • A query format with no SQLAn app can filter with eq, ne, lt, lte, gt, gte, in and contains, then sort, page and pick fields, as structured JSON capped at 10,000 rows.
  • Totals without writing SQLThe same format does count, sum, average, minimum and maximum, with group-by and having.
  • SQL, saved and fencedSQL can be saved against a base and re-run by id. Ad-hoc SQL is rewritten to the caller's own schema, and multiple statements, DROP and TRUNCATE are refused.

Getting data in and out

CSV in, CSV out, rows one at a time or five hundred at a time, and a backup nobody has to remember to run.

  • A CSV becomes a tableHeaders become field names. Blank headers are named for you, duplicates get a suffix, and the invisible marker some spreadsheet exports add is stripped.
  • Types guessed from the valuesAll whole numbers gives INTEGER, all decimals NUMERIC, all true/false/yes/no BOOLEAN, otherwise TEXT. A column of 1s and 0s reads as whole numbers.
  • Where the import stops10,000 rows and 10 MB. Rows go in batches of 500 inside one transaction, and the whole import is rolled back if any batch fails.
  • Download the table as CSVThe download uses the same filters, sorting and chosen fields as the grid on screen.
  • Rows, six waysAdd one, change one, delete one, add many at once, clear the table, or ask for a live row count without fetching any rows.
  • Bulk insert is all or nothingBatches of 500 inside a single transaction, rolled back entirely on any failure.
  • Backed up every nightThe service registers its own schedule, 18:00 UTC daily, and exports only bases changed since their last backup. Seven daily, four weekly and three monthly copies are kept.
  • Outside databases, savedConnection details for a PostgreSQL, MySQL or SQL Server database can be stored, and a PostgreSQL one really tested. No base reads through one yet.

Who can open a base, and what they may do

Access is a stored record called a contract. Nothing reaches a row without one, the base's owner included.

  • Three kinds of contractSteward, the base's owner; access, a direct grant; delegation, a grant handed down. Six states, from draft to expired, so a grant can be prepared before it takes effect.
  • The owner holds one tooCreating a base issues the creator a steward contract with full permissions. Every read and write is checked against a contract, theirs included.
  • Four permissions, checked every timeRead, insert, update and delete, tested on every row operation. They can be held by a person, an app, a service account, a system account, a database administrator, or the public.
  • A grant can be narrowedLimit it to named tables, give it a start date and an expiry date, and choose one of four rules for what happens to grants beneath it when it is withdrawn.
  • Handed on, five levels deepA child grant can never hold a permission its parent lacks or reach a table outside the parent's list, and every check walks the chain back to the root.
  • Narrowing cascades, widening is refusedChildren holding more than the parent are shrunk or suspended automatically, and the change is kept with the old value, the new one, the reason and who asked.
  • A permission sheet per tableA contract can carry per-table permissions: this table readable, that one readable and writable. Once the sheet exists, any table not on it is closed.
  • Down to the rowFour rule shapes: everyone, rows the caller owns, the caller's organisation, or a named role in a named user table. If rules apply but none resolves, nothing is returned.

Calling it from software you already run

A published app, a timetable system or a script reaches a base through the same 108 endpoints, documented at /api/docs behind a username and password.

  • Endpoints for published appsAn app reads and writes under /api/app-data/: list, find, aggregate, create, update and delete. Browser preflight is answered, so a web page can call them directly.
  • Three ways to be identifiedAn anonymous visitor to a published app, a trusted service that has already signed the person in, or a signed-in person acting for themselves.
  • Acting for someone else is gatedClaiming to act as another person needs the shared service token; without it the claim is ignored. Anonymous app traffic needs that token and an identity minted upstream.
  • Sign-in, and a second credentialCallers present an AWS Cognito bearer token, verified on every protected request. Some routes also take a short signed token, which is how background jobs run.
  • An app's access in one callReady-made calls build the usual chain: a public grant on the base, then a delegation to the app limited to named tables. One call withdraws all of it.
  • Ask what you may useAn app or workspace can list the bases it is allowed to use, with the permissions each grant carries and how many tables each base holds.
  • Enough schema to write code againstOne call returns a table's column list plus three sample rows. The list of every base and its tables is deliberately locked to internal services only.
  • One shape of answer, named errorsEvery response carries a message and a data payload, paged ones carry the total, and failures return a stable code such as UNSUPPORTED_COLUMN_TYPE or CSV_TOO_MANY_ROWS.

History, undo and oversight

What happened, who did it, and a way to stop everything at once.

  • A recycle bin, seven daysTables, fields and rows deleted in the last seven days are listed, and any of them can be put back.
  • Deleting hides, it does not eraseA deleted row sets a date and every read skips it; a deleted table and its columns stay in PostgreSQL. Emptying the recycle bin is what actually removes them.
  • A whole base can come backA base can be deleted, listed among the deleted ones, and restored.
  • An audit logWho did what to which base, filtered by base, person, action type and date range. Base creation, row changes, bulk imports and the whole life of a contract are recorded.
  • An administrator's dashboardTotal bases and contracts, active apps, draft contracts waiting, contracts expiring in 7 and 30 days, bases untouched for 90 days, and the largest bases.
  • Freeze a baseOne call refuses every data operation on it, for everyone including the owner, and blocks new grants until it is unfrozen.
  • Admin rights checked elsewhereAdministrator endpoints verify org-admin standing against the organisation's own records, and refuse the request if that lookup is unavailable.
  • Every request logged centrallyMethod, url, status, caller and both bodies go to a central stream, truncated at 400 KB, with health probes skipped and the whole thing off in local development.

The parts that are bigger than they look.

Nothing reaches a row without a contract

A contract is a stored record saying who may read, add, change or delete rows in a base, optionally only in named tables, optionally only certain rows, optionally until a date. Creating a base issues its owner one automatically, so even the owner's own reads are checked against a contract before they run. Someone can be granted access by email address, looked up against the organisation's own membership records rather than trusted from the request, and an optional diagnostic log can record every permission decision for a set number of days.

The list of bases, with a column showing how many tables each holds and how many contracts grant access to it My Bases

The nightly backup is a point-in-time copy you can check

A schedule the service creates for itself feeds a queue, drained by a worker that records every message id, so a repeated delivery is skipped instead of producing a second backup. Every table in a base is exported inside one repeatable-read transaction with the transaction id stored, so the backup is provably a single instant, and rows stream out as gzipped CSV in parts rather than being held in memory. Each run writes a manifest with per-table row counts, byte sizes and a SHA-256 hash computed over all of it.

One call can stand up a base with its access already granted

A single request creates a base, the owner's steward contract, an access contract for the project and a sub-contract for each app underneath it. Another copies every row from one base into another keeping row ids and id sequences, so links survive the copy intact. Because some routes accept a short signed token as well as a person's sign-in, a program can do all of this with nobody watching.

What it does not do.

Worth knowing before you plan around it.

  • No formula or calculated fields, no rollups and no lookup fields. A test in the code deliberately checks that no formula field type exists.
  • No attachment or file field. The only file that goes in is a CSV that becomes a new table, and the only file that comes out is a CSV of a table.
  • No automations, triggers, webhooks or scheduled actions on data. Nothing inside a base fires when a row changes.
  • No saved views. Filters, sorting, chosen fields and grouping are sent fresh with every request, and nothing about a view is stored.
  • Backups go out but do not come back. There is no restore endpoint in the service, so bringing a base back from a backup is not something the API can do today.
  • The access rules have edges worth knowing. Three of the seven permissions on a contract — create table, alter schema, drop object — are recorded, inherited and reported on the dashboard, but no endpoint checks them yet: adding or removing a field is gated on owning the base or holding read access, and creating or deleting a table is owner-only. Row-level rules apply on the row endpoints and the published-app endpoints only, not to the grid, the CSV download or saved SQL. And the CSV download is restricted to the base owner, so someone who can read a table through a grant cannot download it.

Free for teachers.

Sign up, get credits every month, make something today.