{"id":15173806,"url":"https://github.com/technologiestiftung/stadtpuls-api","last_synced_at":"2025-10-01T10:31:46.251Z","repository":{"id":36991572,"uuid":"355817008","full_name":"technologiestiftung/stadtpuls-api","owner":"technologiestiftung","description":"API of stadtpuls.com, an open IoT platform for storing and visualizing sensor data and making it accessible via a REST API ","archived":true,"fork":false,"pushed_at":"2023-02-17T03:31:46.000Z","size":2455,"stargazers_count":6,"open_issues_count":11,"forks_count":0,"subscribers_count":6,"default_branch":"main","last_synced_at":"2024-12-17T17:07:24.700Z","etag":null,"topics":["citylab-berlin","iot","sensors","stadtpuls"],"latest_commit_sha":null,"homepage":"https://stadtpuls.com/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/technologiestiftung.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-04-08T08:08:20.000Z","updated_at":"2023-02-20T07:40:02.000Z","dependencies_parsed_at":"2023-02-18T03:46:05.464Z","dependency_job_id":null,"html_url":"https://github.com/technologiestiftung/stadtpuls-api","commit_stats":{"total_commits":351,"total_committers":8,"mean_commits":43.875,"dds":"0.18803418803418803","last_synced_commit":"bf4139f65e4c9a1a6355984aae211eec1eb12b60"},"previous_names":["technologiestiftung/next-iot-hub-api"],"tags_count":31,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technologiestiftung%2Fstadtpuls-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technologiestiftung%2Fstadtpuls-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technologiestiftung%2Fstadtpuls-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technologiestiftung%2Fstadtpuls-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/technologiestiftung","download_url":"https://codeload.github.com/technologiestiftung/stadtpuls-api/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234858914,"owners_count":18897834,"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":["citylab-berlin","iot","sensors","stadtpuls"],"created_at":"2024-09-27T11:02:34.211Z","updated_at":"2025-10-01T10:31:40.904Z","avatar_url":"https://github.com/technologiestiftung.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![](https://img.shields.io/badge/Built%20with%20%E2%9D%A4%EF%B8%8F-at%20Technologiestiftung%20Berlin-blue)\n\n# Stadtpuls.com API\n\nThis is an fastify based API layer that is used by [technologiestiftung/stadtpuls-frontend](https://github.com/technologiestiftung/stadtpuls-frontend). It does:\n\n- Issuing and maintaining authtokens for verified users\n- Recieving POST requests from external sources via The Things Network (TTN) and HTTP for posting them on the users behalf\n- Recieving GET requests from users to provide access to sensors and their records\n- Wrap supabases signup and login functionality to allow users to provide a username on signup\n\nWithin this repo you also can find:\n\n- Code for running a local version of supabase which is also used in integration tests\n- Code for provisioning the database. We are SQL scripts only. The workflow is not yet finally defined. There is no fixed way of doing schema migrations yet. Possible tools could be [dbmate](https://github.com/amacneil/dbmate) or once it is stable [the new supabase cli](https://github.com/supabase/cli/tree/new)\n- Code for having a small React client to test interaction with this API\n- Code for running a Node.js MQTT client for further explorations\n\nThe API is deployed using docker on render.com\n\n## Setting Up…\n\nTo get the project ready you need to do some tasks.\n\n- Create a supabase project\n- Get your service key and `postgresql://…` connection string\n- Add your service role key to `.env`\n- Provision the dev database\n  - use the scripts `stadtpuls-supabase/supabase-docker-compose/dockerfiles/postgres/docker-entrypoint-initdb.d/` to give your DB the final touches. Watch out: 00-initial-schema.sql, 01-auth-schema.sql and 02-storage-schema.sql are covered by supabase. You don't need these when working with the cloud. The other SQL scripts\n    - create replication of users into the public users table (like mentioned in their [docs](https://supabase.io/docs/guides/auth#create-a-publicusers-table))\n    - disable realtime for all non public tables (see also the link above on the why to do this)\n    - enable row level security on all tables\n    - create delete cascades\n    - create remote procedure calls that allow a user to delete a his account\n\n## Crypto On Tokens\n\nThe user can request a JWT (authtokens) and gets a token based on the jwt secret from supabase. This token gets hashed and is used as primary key for the table authtokens. WARNING: This token can also be used to access the supabase API. If you don't want that you need to use a different secret for the signing of the authtokens in `src/lib/authtokens.ts`. This also needs some refactoring of the usage of jwt.sign which currently uses the fastify-jwt plugin.\n\nWhen a request over TTN, HTTP, or any other integration, comes in we take the token and verify it. Then we look up if there is a token that is aimed at the project and user id encoded in the token. If we find it we can compare the hash (primary key id) against the incoming token. If they match the request is verified and we insert records on the users behalf. If does not match it was deleted or never create in the first place and is not allowed to add records.\n\n## Development\n\nYou need Docker and Node.js.\n\nTo start your local redis database run the following commands:\n\n```bash\ngit clone https://github.com/technologiestiftung/stadtpuls-redis\ncd stadtpuls-redis/\ndocker composse up --detach\n```\n\nTo start you local copy of supabase run the following steps:\n\n```bash\ngit clone https://github.com/technologiestiftung/stadtpuls-supabase\ncd stadtpuls-supabase/supabase-docker-compose\ncp .env.example .env\nmkdir dockerfiles/postgres/pg-data\ndocker compose up --detach\n```\n\nWhen your supabase instance is running you can proceed. Test if the supabase is by running the following command. Make sure to replace `\u003cYOUR ANON KEY\u003e` with the anon key you can find in `stadtpuls-supabase/supabase-docker-compose/dockerfiles/kong/kong.yml` at the bottom. The port may change based on `KONG_PORT` in `stadtpuls-supabase/supabase-docker-compose/.env`.\n\n```bash\ncurl http://localhost:8000/rest/v1/ \\\n  -H \"apikey: \u003cYOUR ANON KEY\u003e\"\n```\n\nTo start your local copy of the API create your `.env` file in the root of the repository `cp .env.example .env` and update the values. You can find them in `stadtpuls-supabase/supabase-docker-compose/.env` and `stadtpuls-supabase/supabase-docker-compose/dockerfiles/kong/kong.yml`. Use the `KONG_PORT` for your `SUPABASE_URL` (`http://localhost:\u003cKONG_PORT\u003e`)\n\n```bash\ncp .env.example .env\nnvm install\nnpm ci\nnpm run dev\n```\n\n## Making Requests\n\nWhen running the API you will see all possible routes in the output of your terminal. Test if it is running by making a call to unprotected routes.\n\n```bash\ncurl http://localhost:4000/\ncurl http://localhost:4000/api\ncurl http://localhost:4000/api/v3\ncurl http://localhost:4000/api/v3/sensors\ncurl http://localhost:4000/api/v3/sensors/:sensorId/records\ncurl http://localhost:4000/api/v3/sensors/:sensorId/records/:recordId\n```\n\nTo make calls to protected routes you will need a supabase user token (created by supabase when you signup or login). The following routes can be called with supabase user tokens:\n\n```plain\nGET\t/api/v\u003cAPI_VERSION\u003e/authtokens\nPOST\t/api/v\u003cAPI_VERSION\u003e/authtokens\nDELETE\t/api/v\u003cAPI_VERSION\u003e/authtokens\n```\n\nThe following routes need an auth token created by this API.\n\n```plain\nPOST\t/api/v\u003cAPI_VERSION\u003e/integrations/ttn/v3\nPOST\t/api/v\u003cAPI_VERSION\u003e/sensors/:sensorId/records\n```\n\n### Create an Auth Token\n\nFirst you need to signup or login.\n\n```bash\n# signup\ncurl --location --request POST 'http://localhost:8000/auth/v1/signup' \\\n--header 'apikey: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwLCJhdWQiOiIiLCJzdWIiOiIiLCJyb2xlIjoiYW5vbiJ9.sUHErUOiKZ3nHQIxy-7jND6B80Uzf9G4NtMLmL6HXPQ' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"email\": \"me@me.com\",\n    \"password\": \"1234password\"\n}'\n# or login\ncurl --location --request POST 'http://localhost:8000/auth/v1/token?grant_type=password' \\\n--header 'apikey: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwLCJhdWQiOiIiLCJzdWIiOiIiLCJyb2xlIjoiYW5vbiJ9.sUHErUOiKZ3nHQIxy-7jND6B80Uzf9G4NtMLmL6HXPQ' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n  \"email\": \"me@me.com\",\n  \"password\": \"1234password\"\n}'\n```\n\nYou will get an response that contains an `access_token` property. That can be used to create (POST), rotate (PUT), get (GET) and delete (DELETE) our auth tokens.\n\n```bash\n# get all existing tokens\ncurl --location --request GET 'http://localhost:4000/api/v3/authtokens?projectId=61' \\\n--header 'Authorization: Bearer \u003cYOUR SUPABASE ACCESS TOKEN\u003e'\n# create a new token\ncurl --location --request POST 'http://localhost:4000/api/v3/authtokens' \\\n--header 'Authorization: Bearer \u003cYOUR SUPABASE ACCESS TOKEN\u003e' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"description\": \"my fancy token\"\n}'\n# delete a token\ncurl --location --request DELETE 'http://localhost:4000/api/v3/authtokens' \\\n--header 'Authorization: Bearer \u003cYOUR SUPABASE ACCESS TOKEN\u003e' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n\"tokenId\": 28,\n}'\n```\n\nOnce you created a new token via POST you can move on to posting records.\n\n### POST Records via HTTP\n\nTo post data via HTTP you need to optain an auth token like described above. Then you can POST data.\n\n```bash\ncurl --location --request POST 'http://localhost:4000/api/v3/sensors/14/records' \\\n--header 'Authorization: Bearer \u003cYOUR AUTH TOKEN\u003e' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"latitude\": 52.483107,\n    \"longitude\": 13.390679,\n    \"altitude\": 30,\n    \"measurements\": [\n        1,\n        2,\n        3\n    ]\n}'\n```\n\n### POST Records via TTN\n\nYou will need an auth token like described above. Then you can hook up your TTN device to the webhooks in https://eu1.cloud.thethings.network/console/. See our extended documentation on https://stadtpuls.com for further infos.\n\n## Testing\n\nTo test the API you need to run the integration tests. You can do this by running `npm test`. Make sure your local supabase is running. Currently the tests use the environment variables from `.env.test`\n\n## Running with Docker\n\nYou can run the stadtpuls-api with docker in several ways.\n\n1. Attaching to an already existing local subase instance.\n2. Running within your supabase setup.\n3. Running with a remote supabase project.\n\nTake a look at [the hub.docker.com page](https://hub.docker.com/repository/docker/technologiestiftung/stadtpuls-api) of the image to see which tag to use. Don't use the latest tag for production.\n\n### Attaching to an already existing local supabase instance\n\nFor attaching to the already existing instance use the following `docker-compose.yml`. You should adjust the environment variables to your needs and then run\n\n```bash\n# MacOS \u0026 Windows\ndocker compose up\n# Linux\ndocker-compose up\n```\n\n```yml\nversion: \"3\"\nservices:\n  stadtpuls-api:\n    environment:\n      PORT: 4000\n      SUPABASE_URL: http://kong:8000\n      SUPABASE_SERVICE_ROLE_KEY: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYyNzIwNzUzMiwiZXhwIjoxNjkwMjc5NTMyLCJhdWQiOiIiLCJzdWIiOiIiLCJyb2xlIjoic2VydmljZV9yb2xlIn0.hfdXFZV5PdvUdo2xK0vStb1i97GJukSkRqfwd4YIh2M\n      SUPABASE_ANON_KEY: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwLCJhdWQiOiIiLCJzdWIiOiIiLCJyb2xlIjoiYW5vbiJ9.sUHErUOiKZ3nHQIxy-7jND6B80Uzf9G4NtMLmL6HXPQ\n      JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long\n      ISSUER: stadtpuls.com\n      LOG_LEVEL: info\n      SUPABASE_MAX_ROWS: 1000\n      DATABASE_URL: postgres://postgres:your-super-secret-and-long-postgres-password@localhost:5432/postgres\n    image: \"technologiestiftung/stadtpuls-api:latest\"\n    ports:\n      - \"4000:4000\"\nnetworks:\n  default:\n    external: true\n    name: supabase_default\n```\n\n### Running within your supabase setup\n\n- Copy the whole service `stadtpuls-api` to the file `stadtpuls-supabase/supabase-docker-compose/docker-compose.yml`.\n- Dont copy the network part.\n- Adjust the `SUPABASE_URL` to (TBD)\n- Adjust the `DATABASE_URL` to (TBD)\n\n```bash\ncd stadtpuls-supabase/supabase-docker-compose/\n# if you had the whole setup already running\ndocker compose down \u0026\u0026 rm -rf dockerfiles/postgres/pg-data/ \u0026\u0026 mkdir dockerfiles/postgres/pg-data\n# MacOS \u0026 Windows\ndocker compose up --build --force-recreate\n# Linux\ndocker-compose up  --build --force-recreate\n```\n\n### Running with a remote supabase project\n\n- Adjust the `SUPABASE_URL`\n- Adjust the `DATABASE_URL`\n- Adjust the `SUPABASE_SERVICE_ROLE_KEY`\n- Adjust the `SUPABSE_ANON_KEY`\n- Adjust the `JWT_SECRET`\n\nYou can find these values under `https://app.supabase.io/project/\u003cYOUR PROJECT ID\u003e/settings/api`\n\n```bash\n# MacOS \u0026 Windows\ndocker compose up\n# Linux\ndocker-compose up\n```\n\n## Contributors\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://fabianmoronzirfas.me/\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/315106?v=4?s=64\" width=\"64px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eFabian Morón Zirfas\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=ff6347\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=ff6347\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://dnsos.info/\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/15640196?v=4?s=64\" width=\"64px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eDennis Ostendorf\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/pulls?q=is%3Apr+reviewed-by%3Adnsos\" title=\"Reviewed Pull Requests\"\u003e👀\u003c/a\u003e \u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=dnsos\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=dnsos\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://vogelino.com/\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/2759340?v=4?s=64\" width=\"64px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eLucas Vogel\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/pulls?q=is%3Apr+reviewed-by%3Avogelino\" title=\"Reviewed Pull Requests\"\u003e👀\u003c/a\u003e \u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=vogelino\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=vogelino\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/lucasoeth\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/43838158?v=4?s=64\" width=\"64px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003elucasoeth\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=lucasoeth\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/julizet\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/52455010?v=4?s=64\" width=\"64px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eJulia Zet\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/technologiestiftung/stadtpuls-api/commits?author=julizet\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n\n## Credits\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\n      \u003ca src=\"https://citylab-berlin.org/de/start/\"\u003e\n        \u003cbr /\u003e\n        \u003cbr /\u003e\n        \u003cimg width=\"200\" src=\"https://citylab-berlin.org/wp-content/uploads/2021/05/citylab-logo.svg\" /\u003e\n      \u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      A project by: \u003ca src=\"https://www.technologiestiftung-berlin.de/\"\u003e\n        \u003cbr /\u003e\n        \u003cbr /\u003e\n        \u003cimg width=\"150\" src=\"https://citylab-berlin.org/wp-content/uploads/2021/05/tsb.svg\" /\u003e\n      \u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      Supported by: \u003ca src=\"https://www.berlin.de/rbmskzl/\"\u003e\n        \u003cbr /\u003e\n        \u003cbr /\u003e\n        \u003cimg width=\"80\" src=\"https://citylab-berlin.org/wp-content/uploads/2021/12/B_RBmin_Skzl_Logo_DE_V_PT_RGB-300x200.png\" /\u003e\n      \u003c/a\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnologiestiftung%2Fstadtpuls-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechnologiestiftung%2Fstadtpuls-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnologiestiftung%2Fstadtpuls-api/lists"}