{"id":18789704,"url":"https://github.com/crewdevio/reactivedb","last_synced_at":"2025-04-13T14:05:59.689Z","repository":{"id":112325594,"uuid":"417971949","full_name":"crewdevio/reactivedb","owner":"crewdevio","description":"Realtime database for realtime apps","archived":false,"fork":false,"pushed_at":"2024-01-31T21:07:50.000Z","size":1571,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-11T02:41:35.210Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/crewdevio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-10-16T23:27:33.000Z","updated_at":"2024-04-26T08:21:02.000Z","dependencies_parsed_at":"2024-01-06T01:04:28.805Z","dependency_job_id":"2ff9a125-0592-45eb-9541-957dba4474c8","html_url":"https://github.com/crewdevio/reactivedb","commit_stats":{"total_commits":35,"total_committers":1,"mean_commits":35.0,"dds":0.0,"last_synced_commit":"274d0a1f47cd4eabd309e8e0c65e259a039069f2"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crewdevio%2Freactivedb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crewdevio%2Freactivedb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crewdevio%2Freactivedb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crewdevio%2Freactivedb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/crewdevio","download_url":"https://codeload.github.com/crewdevio/reactivedb/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248545246,"owners_count":21122099,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T21:08:21.676Z","updated_at":"2025-04-13T14:05:59.668Z","avatar_url":"https://github.com/crewdevio.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ReactiveDB realtime database/backend\n\nfor mongodb (will support postgres, and DenoKV in the future) heavy inspired on firebase, pocketbase and supabase.\n\n\u003e [!IMPORTANT]\n\u003e (READ): The documentation is still under development so not all the functionalities of reactivedb are yet documented.\n\n\u003e History: The idea of ReactiveDB was born at the beginning of 2020 while I was working for a software company, the project I was in needed to have data synchronization between multiple users, originally we built everything around firebase which worked perfectly, but for client requirements it was necessary to have the entire infrastructure running on my own servers locally, so I was faced with the task of thinking of a solution that would meet these requirements and at the same time allow us not to have to change everything that was already closely coupled to the design from firebase, so ReactiveDB was born from here.\n\n## Original diagram\n\n![original diagram](./static/original-diagram.png)\n\n## So what is ReactiveDB?\n\nReactiveDB aims, like firebase, supabase, poketbase, to provide a fast backend structure and listen to the changes that occur in the database in real time, but ReactiveDB takes the idea of being a framework/library which makes it easy to integrate it into a project already running or also start a project from scratch.\n\n\u003e example\n\n```ts\nimport { ReactiveCore, Crypto } from \"https://deno.land/x/reactivedb/mod.ts\";\nimport { load } from \"https://deno.land/std/dotenv/mod.ts\";\n\n// load envs\nconst {\n  REACTIVE_SERVER_PORT,\n  REACTIVE_JWK_BASE_64,\n  REACTIVE_DB_CONNECTION,\n  REACTIVE_DB_NAME,\n} = await load();\n\nconst crypt = new Crypto({ name: \"HMAC\", hash: \"SHA-512\" }, true, [\n  \"sign\",\n  \"verify\",\n]);\n\nconst secretKey = await crypt.importFromJWKBase64(REACTIVE_JWK_BASE_64);\n\n// start reactivedb\nawait ReactiveCore({\n  connection: REACTIVE_DB_CONNECTION, // mongodb+srv://\u003cusername\u003e:\u003cpassword\u003e@\u003ccluster-address\u003e/?retryWrites=true\u0026w=majority\n  port: Number(REACTIVE_SERVER_PORT), // 4000\n  database: REACTIVE_DB_NAME, // \u003cdatabase-name\u003e\n  CLSDefinition: rules,\n  secretKey,\n});\n```\n\nWith these few lines of code you can have a complete backend and real-time connection with the collections within the database.\n\nIf you have a collection let's say \"Users\" in the database, reactive db will automatically create a rest api and a websockets channel to read and write data automatically.\n\n\u003e example: client deno\n\n```ts\nimport {\n  createClient,\n  Auth,\n} from \"https://deno.land/x/reactivedb/client/mod.ts\";\n\nconst url = \"http://localhost:8080\";\n\nconst auth = new Auth(url);\n\n// login and auth\nconst token = await auth.loginWithEmailAndPassword(\n  \"email@example.com\",\n  \"12345678\"\n);\n\n// create instance\nconst ReactiveDB = createClient(url, token!);\n\n// create a client\nconst client = ReactiveDB();\n\n// connecto to a channel or collection from database\nclient.connectTo(\"Users\", () =\u003e console.log(\"connected to Users\"));\n\n// listen events from collection\nclient.on(\"child_added\", (data, event) =\u003e {\n  console.log({ data, event });\n});\n\n// perform actions if connection close\nclient.onClose(() =\u003e {\n  console.log(\"disconnected from Users\");\n});\n```\n\nand also exist a Restfull api to all collections\n\n\u003e example\n\n```curl\n\nGET: http://localhost:8080/v1/Users -\u003e get all docs from Users collections\n\nPOST: http://localhost:8080/v1/Users '{ \"name\": \"Jhon Doe\", \"password\": \"1234\" }' -\u003e push new data on Users collection\n\nGET: http://localhost:8080/v1/Users/12345 -\u003e get doc on collection by doc id\n\nPUT, PATCH: http://localhost:8080/v1/Users/12345 '{ \"name\": \"Jhon Smith\", \"password\": \"1234\" }' -\u003e update the doc from the collection by doc id\n```\n\nThis rest api is automatic for all collections within the database, using the api automatically sends a notification to clients that are listening for changes.\n\n## The basics\n\n### How to start?\n\nfirst install ReactiveDB:\n\n```ts\nimport { ReactiveCore } from \"https://deno.land/x/reactivedb/mod.ts\";\n```\n\nor using Trex\n\n```console\ntrex install reactivedb\n```\n\nnow create a database on mongodb atlas or use a local mongodb server:\n\n```ts\nimport { ReactiveCore, Crypto } from \"https://deno.land/x/reactivedb/mod.ts\";\nimport { load } from \"https://deno.land/std/dotenv/mod.ts\";\n\n// load envs - we strongly recommend using environment variables\nconst {\n  REACTIVE_SERVER_PORT,\n  REACTIVE_JWK_BASE_64,\n  REACTIVE_DB_CONNECTION,\n  REACTIVE_DB_NAME,\n} = await load();\n```\n\nthen generate a secure CryptoKey:\n\n```ts\nimport { Crypto } from \"https://deno.land/x/reactivedb/mod.ts\";\n\nconst crypto = new Crypto({ name: \"HMAC\", hash: \"SHA-512\" }, true, [\n  \"sign\",\n  \"verify\",\n]);\n\nawait crypto.generateKey();\n\nconst { toBase64 } = await crypto.exportToJWKBase64();\n\nconsole.log(toBase64()); // eyJrdHkiOiJvY3QiLCJrIjoib040ek5FRmhrVEdkaWJTdHpOMTZwZ1.....\n```\n\nWe recommend saving this token in an environment variable and not sharing it since the token is used to sign and verify JWTs.\n\n`.env`\n\n```env\nREACTIVE_JWK_BASE_64=\"eyJrdHkiOiJvY3QiLCJrIjoib040ek5FRmhrVEdkaWJTdHpOMTZwZ1.....\"\n```\n\n\u003e **Note:** Each CryptoKey is unique, so if it is changed to a new one, it will not be possible to verify the tokens created with the previous CryptoKey.\n\nnow load the CryptoKey:\n\n```ts\nimport { ReactiveCore, Crypto } from \"https://deno.land/x/reactivedb/mod.ts\";\nimport { load } from \"https://deno.land/std/dotenv/mod.ts\";\n\n// load envs\nconst {\n  REACTIVE_SERVER_PORT,\n  REACTIVE_JWK_BASE_64,\n  REACTIVE_DB_CONNECTION,\n  REACTIVE_DB_NAME,\n} = await load();\n\nconst crypt = new Crypto({ name: \"HMAC\", hash: \"SHA-512\" }, true, [\n  \"sign\",\n  \"verify\",\n]);\n\n// load the CryptoKey from previous step\nconst secretKey = await crypt.importFromJWKBase64(REACTIVE_JWK_BASE_64);\n\n// start reactivedb\nawait ReactiveCore({\n  connection: REACTIVE_DB_CONNECTION, // mongodb+srv://\u003cusername\u003e:\u003cpassword\u003e@\u003ccluster-address\u003e/?retryWrites=true\u0026w=majority\n  port: Number(REACTIVE_SERVER_PORT), // 4000\n  database: REACTIVE_DB_NAME, // \u003cdatabase-name\u003e\n  CLSDefinition: rules,\n  secretKey,\n});\n```\n\n## Advanced concepts\n\n### ReactiveDB Functions\n\nwith ReactiveDB you can create your own endpoints called \"Functions\" these endpoints are structured using file system routing.\n\nTo create functions you just have to create a \"functions\" folder at the root of the project.\n\n`functions/`\n\nThis is the anatomy of a reactive db function:\n\n`functions/users.[get].ts`\n\n- users: the name of the `endpoint/function` -\u003e http://localhost:8080/users, `index` name is transformed to `/`\n\n- [ get ] : the http methods supported by the `endpoint/function` -\u003e all methods supported: get, post, put, delete, all.\n  You can combine several methods for the same function: `users.[get,post,put].ts` or allow all methods `users.[all].ts`\n- `.ts` -\u003e file extension, supported file extensions: ts, js, tsx, jsx\n\nInside of the function looks like this:\n\n`functions/user.[get].ts`\n\n```ts\nimport type {\n  Context,\n  Utilities,\n  Middleware,\n} from \"https://deno.land/x/reactivedb/mod.ts\";\nimport {\n  Handler,\n  HandlerMiddlewares,\n} from \"https://deno.land/x/reactivedb/mod.ts\";\n\n// middlewares for this function\nexport const middlewares = HandlerMiddlewares([\n  async (ctx, next) =\u003e {\n    console.log(ctx.request.ip);\n\n    await next();\n  },\n]);\n\n// function\nexport default async function Index(context: Context, utils: Utilities) {\n  try {\n    const cursor = await utils.Database.collection(\"Users\");\n\n    const results = await cursor\n      .find(undefined, { noCursorTimeout: false })\n      .toArray();\n\n    utils.Events.post({\n      to: \"Users\",\n      data: [],\n      event: \"child_added\",\n    });\n\n    context.response.status = 200;\n    context.response.body = results;\n  } catch (error) {\n    console.log(error);\n  }\n}\n```\n\nNow we will explain each part:\n\n\u003e **Note**: You can only export a single function per file and it must be exported by default:\n\n```ts\nexport default async function Fn() {\n  ....\n}\n```\n\nEach function receives two parameters: `Context` and `Utilities`:\n\n- `Context`: In the context are the `Response` and the `Request` of each http request, reactivedb is built on top of the Oak framework so it is managed like any app made in [Oak](https://deno.land/x/oak/mod.ts?s=Context)\n\n```ts\nexport default async function Fn(context: Context) {\n  const body = context.request.body({ type: \"json\" });\n\n  context.response.status = 200;\n  context.response.body = {\n    ok: true,\n  };\n}\n```\n\n- `Utilities`: The utilities contain the connection to the database to make queries and add data to the collections. They also contain the method to dispatch events for the websocket connections that are listening.\n\n```ts\nexport default async function Fn(context: Context, utils: Utilities) {\n  try {\n    // connect to Users collection\n    const cursor = await utils.Database.collection(\"Users\");\n\n    // get all users from Users collection\n    const results = await cursor\n      .find(undefined, { noCursorTimeout: false })\n      .toArray();\n\n    // send to Users listeners\n    utils.Events.post({\n      to: \"Users\",\n      data: [],\n      event: \"child_added\",\n    });\n\n    context.response.status = 200;\n    context.response.body = {\n      ok: true,\n    };\n  } catch (error) {\n    console.log(error);\n  }\n}\n```\n\n#### Middlewares\n\nThe functions support middlewares, so you can define all the middlewares you need in each function.\n\n```ts\n// middlewares list\nexport const middlewares = HandlerMiddlewares([\n  async (ctx, next) =\u003e {\n    console.log(ctx.request.ip);\n\n    await next();\n  },\n]);\n\nexport default async function Fn(context: Context, utils: Utilities) {\n  ....\n}\n```\n\nIt is important that the next function is called in each middleware, since if it is not done, the execution will not continue to the other middlewares and towards the function.\n\n```ts\nexport const middlewares = HandlerMiddlewares([\n  async (ctx, next) =\u003e {\n    console.log(ctx.request.ip);\n\n    await next(); // call it always\n  },\n]);\n```\n\n\u003e **Note**: You must always export with the name `middlewares` so that the middlewares are loaded before the execution of the function.\n\n```ts\nexport const middlewares = HandlerMiddlewares([\n  ....\n]);\n```\n\n### Collection Level Security (CLS)\n\nReactiveDB implements a simple mechanism to write rules that protect database writes and reads, these rules are heavily inspired by [Cloud Firestore Security Rules](https://firebase.google.com/docs/firestore/security/get-started?hl=en) and [Row Level Security Supabase](https://supabase.com/docs/guides/auth/row-level-security)\n\nCLS allows you to create detailed rules for reading, writing, updating and deleting objects in collections.\n\n\u003e example\n\n```ts\nimport { ReactiveCore, CLSBuilder } from \"https://deno.land/x/reactivedb/mod.ts\";\n\n....\n\nconst rules = CLSBuilder(() =\u003e ({\n  \"/:collection/:id\": (params, db, context) =\u003e {\n    return {\n      async read() {\n        // match and get rule params\n        const { id, collection } = params\u003c\"/:collection/:id\"\u003e();\n\n        const items = db.collection(collection);\n\n        const doc = await items.findOne({ _id: new Bson.ObjectId(id) })!;\n        // get user\n        const userReq = await items.findOne({ uuid: context.uuid })!;\n\n        const { uuid } = context;\n\n        if (!userReq) {\n          // disallow read if not exist this user\n          return false;\n        }\n\n        return true;\n      },\n      write() {\n        return {\n          create() {\n            // allow to create\n            return true;\n          },\n          delete() {\n            return false;\n          },\n          update() {\n            return false;\n          },\n        };\n      },\n    };\n  },\n}));\n\n....\n\nawait ReactiveCore({\n  connection: REACTIVE_DB_CONNECTION, // mongodb+srv://\u003cusername\u003e:\u003cpassword\u003e@\u003ccluster-address\u003e/?retryWrites=true\u0026w=majority\n  port: Number(REACTIVE_SERVER_PORT), // 4000\n  database: REACTIVE_DB_NAME, // \u003cdatabase-name\u003e\n  CLSDefinition: rules,\n  secretKey,\n});\n```\n\n\u003e **Note**: Security rules do not apply to the websockets(for now) connection, they only apply to HTTP API(v1)\n\nyou can create all the rules you want to catch all the conditions you need.\n\nThese are all the tools that are available to define the rules:\n\n- `params`: the parameters of the route to intercept, you can pass the route to match to the function in the form of a generic type, so that it is typed and returns the parameters the same as the route.\n\n```ts\nconst { id, collection } = params\u003c\"/:collection/:id\"\u003e();\n        ^^      ^^-----------------------^^      ^^\n        |                                        |\n        ------------------------------------------\n```\n\n- `db`: contain the connection to the database to make queries.\n\n```ts\nconst items = db.collection(collection);\n\nconst doc = await items.findOne({ _id: new Bson.ObjectId(id) })!;\n```\n\n- `context`: contains the information of the user who is accessing the database.\n\n```ts\ncontext; // { uuid: string; email: string; auth: boolean; }\n```\n\nWrite rules allow you to create individual rules to update, create, and delete data. but if you want you can make a single global rule for writing.\n\n```ts\nwrite() {\n  return {\n    create() {\n      return true;\n    },\n    delete() {\n      return false;\n    },\n    update() {\n      return false;\n    },\n  };\n}\n\n// or\n\nwrite() {\n  return true;\n}\n```\n\nThe read function can be asynchronous or not, but it must always return a boolean data:\n\n```ts\nread(): Promise\u003cboolean\u003e | boolean;\n```\n\nFor the write function, the function itself can be asynchronous or not, but it always has to return either a boolean data or a definition of write, update, delete:\n\n```ts\nwrite(): Promise\u003cboolean\u003e | Promise\u003cWriteDefinition\u003e | boolean | WriteDefinition;\n```\n\nfor write functions for writes these may or may not be asynchronous but must always return a boolean value:\n\n```ts\ncreate(): Promise\u003cboolean\u003e | boolean;\n\ndelete(): Promise\u003cboolean\u003e | boolean;\n\nupdate(): Promise\u003cboolean\u003e | boolean;\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrewdevio%2Freactivedb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrewdevio%2Freactivedb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrewdevio%2Freactivedb/lists"}