Poor Man's Back End-as-a-Service (BaaS), Similar to Firebase/Supabase/Pocketbase

原始链接: https://github.com/zserge/pennybase

Pennybase is a minimalistic, self-hosted Backend-as-a-Service (BaaS) written in under 1000 lines of Go, offering core features like data storage, authentication, authorization, and real-time updates without external dependencies. It stores data in human-readable CSV files with versioning, maintaining an in-memory index for fast access. Data structures are defined via a `_schemas.csv` file, mapping JSON fields to CSV columns with validation rules. Pennybase provides a REST API for managing resources (GET, POST, PUT, DELETE) and streaming real-time updates via Server-Sent Events. Authentication is handled with session cookies or Basic Auth. Role-Based Access Control (RBAC) and ownership-based permissions are implemented through a `_permissions.csv` file. User credentials and roles are stored in `_users.csv`, requiring manual user creation. Beyond the API, Pennybase serves static assets and renders Go templates, providing user context and authorization checks. A hook function enables extending functionality with custom logic triggered on data creation, updates, or deletion.

This Hacker News thread discusses "Poor Man's Back End-as-a-Service," a minimalist alternative to Firebase/Supabase/Pocketbase. Concerns are raised about the use of CSV for data storage (particularly user credentials with SHA-256 hashing) instead of SQLite. While SQLite is mentioned to be a good alternative, Golang's standard library requires a separate driver. Some users expressed the potential for regex injection. Alternative approaches are suggested, including using the File System Access API for local data storage in browsers, which offers a cloudless app experience. However, this approach faces limitations with multi-device usage and browser compatibility. Others question the project's novelty, suggesting contributions to existing projects like Pocketbase. Some appreciate the project's simplicity, highlighting its suitability for small-scale applications manageable with basic tools. Trailbase and the growing number of "pennybase" tools are mentioned as similar options. The thread concludes with suggestions for using well-established frameworks like Laravel, Rails, or Django.
相关文章

原文

Poor man's Backend-as-a-Service (BaaS), similar to Firebase/Supabase/Pocketbase

It implements core backend features in less than 1000 lines of Go code, using only standard library and no external dependencies:

  • File-based storage using CSV with versioned records
  • REST API with JSON responses
  • Authentication with session cookies and Basic Auth
  • RBAC & ownership-based permissions
  • Real-time updates via SSE
  • Schema validation for numbers/text/lists
  • Template rendering with Go templates

Data stored in human-readable CSVs, one row per record. Data storage is append-only, with each update creating a new version of the record. The latest version is always used for reads. For faster lookups and updates, Pennybase maintains an in-memory index of the latest versions (offsets from the beginning of the CSV file).

We agree that the first column in CSV is always the record ID, and the second column is the version number. The rest of the columns are data fields.

To put JSON resources into such CSV format, Pennybase uses a simple schema definition in _schemas.csv that maps JSON fields to CSV columns. Typically it looks like this:

s1,1,_permissions,_id,text,,,^.+$
s2,1,_permissions,_v,number,1,,
s3,1,_permissions,resource,text,,,^.+$
s4,1,_permissions,action,text,,,^.+$
s5,1,_permissions,field,text,,,^.*$
s6,1,_permissions,role,text,,,^.*$
s7,1,_users,_id,text,,,^.+$
s8,1,_users,_v,number,1,,
s9,1,_users,salt,text,,,
s10,1,_users,password,text,,,^.+$
s11,1,_users,roles,list,,,
s12,1,todo,_id,text,,,^.+$
s13,1,todo,_v,number,1,,
s14,1,todo,description,text,0,0,".+"
s15,1,todo,completed,number,0,1,""

Here first column is ID, second is version number (schemas are immutable), then comes the resource/collection name, followed by field name, field type, min/max value for numbers, and validation regex for strings.

For simplicity only text, number and list field type are supported.

Another important file is _users.csv which contains user credentials and roles. It has the same format as other resources, but with a special _users collection name. There is no way to add new users via API, they must be created manually by editing this file:

admin,1,salt,5V5R4SO4ZIFMXRZUL2EQMT2CJSREI7EMTK7AH2ND3T7BXIDLMNVQ====,"admin"
alice,1,salt,PXHQWNPTZCBORTO5ASIJYVVAINQLQKJSOAQ4UXIAKTR55BU4HGRQ====,

Here we have user ID which is user name, version number (always 1), salt for password hashing, and the password itself (hashed with SHA-256 and encoded as Base32). The last column is a list of roles assigned to the user.

One last special file is _permissions.csv which defines access control rules for resources. Each row defines a rule that allows access to a resource:

p1,1,todo,read,,*,"Anyone authenticated can read todos"
p2,1,todo,create,,*,"Anyone authenticated can add new todos"
p3,1,todo,update,owner,"admin,editor","Only owners and users with admin or editor role can update todos"
p4,1,todo,delete,owner,"admin,editor","Only owners and users with admin or editor role can delete todos"

It's very basic role-based access control: when the system needs to perform an action on a resource it checks the matching permission rule (there may be more then one). If the user has one of the roles in the list - permission is granted. Alternatively, if the resource field specified in the rule matches user ID - permission is granted as well (in the example above "owner" is the field of "todo" resource that contains owner user ID). If no rules match - access is denied.

Based on the resources defined in _schemas.csv, Pennybase provides a REST API with the following endpoints:

  • GET /api/{resource}?sort_by={field} - list all records in the resource, optionally sorting them
  • GET /api/{resource}/{id} - get a single record by ID
  • POST /api/{resource} - create a new record (requires "create" permission)
  • PUT /api/{resource}/{id} - update an existing record (requires "update" permission)
  • DELETE /api/{resource}/{id} - delete a record (requires "delete" permission)
  • GET /api/events/{resource} - stream server-side events for a resource (requires "read" permission)

One may use basic auth to authenticate requests, or use session cookies. Session cookies are created by sending a POST request to /api/login with username and password fields in the body. The response will contain a session cookie that can be used for subsequent requests. Calling /api/logout will invalidate the session and remove the cookie.

Pennybase can also serve static assets from the static directory. You can place your HTML, CSS, JavaScript files there and access them via /{filename} URL.

Additionally, Pennybase supports rendering HTML templates using Go's html/template package. You can create a template file in the templates directory and access it via /{filename} URL as well. The following data is available in the templates:

  • .User - the currently authenticated user (or nil if not authenticated)
  • .Store - the Pennybase store instance, for reading or listing resources
  • .Request - the current HTTP request
  • .ID - the ID of the resource being accessed (if applicable)
  • .Authorize - a function to check if the user has permission to perform an action on a resource:
{{ if .User }}
{{ if call .Authorize "my-resource" "read" .ID }}
<div>{{ .Store.Get "my-resource" .ID }}</div>
{{ else }}
<div>You do not have permission to read this resource.</div>
{{ end }}
{{ else }}
<div>Please log in to access this resource.</div>
{{ end }}

Extending Pennybase functionality is possible via hooks. Or, technically, one hook function:

server, err := pennybase.NewServer("data", "templates", "static")
if err != nil {
    log.Fatal(err)
}
server.Hook = func(trigger, resource string, user pennybase.Resource, res pennybase.Resource) error {
    log.Printf("Hook triggered: %s on %s by user %v: %v", trigger, resource, user, res)
    if trigger == "create" && resource == "messages" {
        r["author"] = user["_id"]
        r["created_at"] = time.Now().UTC().Format("2006-01-02T15:04:05Z07:00")
    }
    return nil
}
log.Fatal(http.ListenAndServe(":8000", server))

This hook will be called on every create/update/delete action on any resource. The trigger parameter indicates the action type, resource is the name of the resource being modified, user is the user performing the action, and res is the resource data being modified.

You may perform additional validation or modify the resource data before it is saved. If you return an error from the hook, the action will be aborted and an error response will be sent to the client.

Contributions are welcome, but please make sure the code remains small, clear and correct. Likely, no new features would be added, except for bug fixes, tests and examples.

The code is distributed under MIT license, so feel free to play with it, fork it, do anything you want!

联系我们 contact @ memedata.com