Show HN:开发用户可自选数据存储位置的 SaaS 应用
Show HN: Write SaaS apps where users control where their data is stored

原始链接: https://github.com/wolfoo2931/linkedrecords/

LinkedRecords 是一款专为单页应用(SPA)直接集成而设计的 NoSQL 数据库,无需编写后端代码。它提供了一种简单、灵活且解耦的架构,使开发者能够在卸载复杂后端任务的同时,构建数据驱动的前端应用。 其核心特性在于内置的细粒度授权功能:数据所有者无需遵循通用的后端规则,而是在创建每条记录时指定相应的访问权限。这在确保安全性的同时,使前端开发者能够直接与数据库交互。性能测试表明,文档的创建、检索和列表等操作均保持高效,且不受数据库总规模影响,能够根据用户可见数据实现可预测的扩展。 该系统可通过环境变量进行高度配置,支持将 PostgreSQL 作为主存储,并将 S3 用于存储大型二进制对象。它能与 Auth0 或 Okta 等支持 OpenID Connect(OIDC)的身份验证服务无缝集成。作为结构化数据的安全“存储桶”,LinkedRecords 为传统 SQL 提供了一种现代化的替代方案,在不牺牲安全性和性能的前提下,实现了实时更新并简化了开发体验。

WolfOliver 推出了 **LinkedRecords**,这是一款开源的后端即服务(BaaS),旨在作为 Firebase 和 Convex 的替代方案。 该项目源于开发者构建类似 Google Docs 的实时协作工具的需求,LinkedRecords 利用了 RDF 三元组存储和“读取时模式”(schema-on-read)概念的独特诠释。这种架构创建了一个完全脱离特定领域业务逻辑的后端,使开发者无需编写自定义后端代码即可构建 SaaS 应用。 主要优势包括: * **内置授权:** 安全性已集成到查询系统中。 * **响应式状态:** 在浏览器中提供类似 SQL 的体验,所有查询均会自动更新。 * **可移植性:** 单页应用可以指向任何 LinkedRecords 部署,从而实现应用与后端基础设施的解耦。 * **人工智能兼容性:** 这种结构化且无逻辑的方法对 AI 代理而言非常高效。 该项目目前已开源,开发者邀请社区通过 [linkedrecords.com/getting-started/](https://linkedrecords.com/getting-started/) 的入门教程探索该平台并提供反馈。
相关文章

原文

LinkedRecords is a NoSQL database that you can connect to directly from your single-page application - no backend code required.


Want to build apps with LinkedRecords? Check out the documentation website:


The chart below shows the performance of core operations as the database grows. This is automatically updated after each merge to main.

The load test simulates a realistic document management scenario. Each createDocument operation creates a blueprint of 8 attributes (7 KeyValueAttributes + 1 LongTextAttribute) including the document content, collaborator/reader groups, comments, references, and configuration. The fetchDocuments operation lists all documents for a user, while fetchDocument retrieves a single document with all its associated attributes.

Load Test Performance Chart

The test simulates a multi-tenant environment with three users:

  • User 1 continuously creates documents (in the current test configuration 5,000 iterations).
  • User 2 is the "user under test" who creates one document for every 10 documents User 1 creates, up to 300 documents. At 300 documents, the creation of documents for this user stops.
  • User 3 creates documents occasionally (every 1,000 iterations)

The x-axis shows the total number of documents in the database (owned by all users combined). The y-axis shows the response time in milliseconds.

What's being measured:

Operation Description
createDocument Time to create a new document with all related attributes (content, config, comments, references, collaborator/reader groups). You can see that createDocument is independent of the total amount of documents in the database as well as the amount of documents visible to the user.
fetchDocuments Time to fetch User 2's document list (up to 300 documents they see). You can see that this time depends on the amount of documents visible to the user but not on the total amount of documents (the graph flattens around 3000 documents)
fetchDocument Time to fetch a single document with all related data (content, comments, groups, activity state, references). You can see that this time depends on the amount of documents visible to the user but not on the total amount of documents (the graph flattens around 3000 documents)

You can think of LinkedRecords as a bucket where anyone can sign up and insert data. As long as you don’t share this data with other users or groups, only you can access what you’ve written into it.

In theory, any user could use the LinkedRecords API directly to write and retrieve data. However, this would be inconvenient - just as you wouldn’t expect your users to write SQL queries, you wouldn’t expect them to interact with the LinkedRecords API. A LinkedRecords app is a specialized frontend that hides the API and provides a convenient user interface for accomplishing their tasks.

In the traditional SQL world, inconvenience isn’t the only reason you don’t let users access the database directly - authorization concerns are an even stronger reason. With LinkedRecords, this is no longer an issue: authorization is built directly into the API. This requires a small mindset shift: Instead of defining universal authorization rules in the backend for all records, the user who inserts a data record specifies who can read it.

For the LinkedRecords API, simplicity, flexibility, and a decoupled architecture are the main qualities we strive to achieve.

  • Simplicity: The API should not have many endpoints or methods; instead, it consists of a few fundamental building blocks.
  • Flexibility: The few available endpoints can be composed to support a variety of use cases. The backed in authorization model should allow to implement different authorization use cases (RBAC, ...).
  • Decoupled: LinkedRecords should be decoupled from the single-page applications which use it as data storage

Think of it as SQL you can call directly from your React app without worrying about permissions; it is easier to read than SQL and provides live updates.

LinkedRecords is configured via environment variables. See tables below.

Environment Variable Name Example Description
PGHOST localhost The hostname of the PostgreSQL server.
PGUSER linkedrecords The PostgreSQL user name.
PGPASSWORD xxxx The PostgreSQL password.
PGDATABASE xxxx The PostgreSQL database name.
CORS_ORIGIN ["https://app.example.com", "https://app.example.app"] The content of the cors origin header. If not provided, the value of FRONTEND_BASE_URL will be used.
SERVER_BASE_URL http://localhost:6543 The public URL of the linkedrecords server.
DEFAULT_STORAGE_SIZE_QUOTA 500 The default storage size quota in MB.
QUOTA_COUNT_KV_ATTRIBUTES false If the storage space for KeyValue attributes are deducted from the accountee quota.
QUOTA_COUNT_LT_ATTRIBUTES false If the storage space for LongText attributes are deducted from the accountee quota.
ENABLE_AUTH_RULE_CACHE false Enable cache for authorization lookups. Might require a lot of memory.
SHORT_LIVED_ACCESS_TOKEN_SIGNING xxxx Configuring this is optional but can reduce load on the database because short lived access token will be used for checking access when a client subscribes to attribute changes.

The environment variables in this section are all optional if the configuration described in the section "Public Client Mode" are provided.

If LinkedRecords runs in confidential client mode, then a session token will be stored in an HttpOnly cookie. From a security standpoint this is considered the suggested method. However, this is not possible if the LinkedRecord server and the frontend do not share the same domain. Across different domains the cookie becomes a third-party cookie, so this mode cannot be used.

Environment Variable Name Example Description
FRONTEND_BASE_URL http://localhost:3001 The base URL of the frontend. It will be used for the Access-Control-Allow-Origin HTTP header and is also required for the OpenID connect redirection.
AUTH_ISSUER_BASE_URL https://xxx.us.auth0.com/ The URL of the OIDC issuer. Can be any OpenID Connect compliant identity provider (e.g. Auth0, Okta).
AUTH_CLIENT_ID The client id. Can be obtained from the identity provider.
AUTH_CLIENT_SECRET The client secret. Can be obtained from the identity provider.
AUTH_IDP_LOGOUT true When set to true the user session will be destroyed in the application AND the within the identity provider.
AUTH_COOKIE_SIGNING_SECRET xxxx The secret used to sign cookies.

In case the single-page application is hosted on a different domain than the LinkedRecords server, the single-page application has to store the access token in the browser. In this scenario the following environment variables need to be configured.

Environment Variable Name Example Description
ALLOW_HTTP_AUTHENTICATION_HEADER true Allows public clients to make requests by providing an access token via http authentication header.
AUTH_ISSUER_BASE_URL https://xxx.us.auth0.com/ The URL of the OIDC issuer. Can be any OpenID Connect compliant identity provider (e.g. Auth0, Okta).
AUTH_TOKEN_AUDIENCE your-audience-id LinkedRecords will check the audience specified in the JWT bearer token against the value specified in this field.
AUTH_CLIENT_ID The client id. Can be obtained from the identity provider.

The single-page application needs to initialize the LinkedRecords SDK as shown below:

import LinkedRecords from './src/browser_sdk';

const oidcConfig = {
  client_id: 'your-client-id',
  redirect_uri: window.location.origin + '/callback',
};

// Instantiating LinkedRecords will automatically handle the OIDC redirect callback
const lr = new LinkedRecords(new URL('https://your-backend.com'), oidcConfig);

// To check if the user is authenticated:
// const isAuth = await lr.isAuthenticated();

// To start login flow (e.g., on a button click):
// lr.login();

If S3 is configured it will be used to store blob attribute values. If it is not configured they will be stored in PostgreSQL database. It is recommended to configure S3.

Environment Variable Name Example Description
S3_COPY_FROM_BL_ATTRIBUTE_TABLE false This is used for migration blob storage from postgresql to S3.
S3_ENDPOINT s3.system.svc.cluster.local The hostname of the S3 endpoint.
S3_BUCKET linkedrecords-blobs The name of a bucket. The bucket must exist already.
S3_ACCESS_KEY xxx The access key id to upload blobs to S3.
S3_SECRET_KEY xxx The secret key id to upload blobs to S3.
S3_USE_SSL false Do not use TLS when uploading/downloading to S3.
Environment Variable Name Example Description
PADDLE_NOTIFICATION_SECRET xxxx If paddle is used for upgrading quotas this needs to be the notification secret to verify the signature of the webhook content.
PADDLE_API_URL https://sandbox-api.paddle.com The URL of the paddle api.
PADDLE_API_KEY xxx the paddle API key.
联系我们 contact @ memedata.com