Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Beeline Firebase talk - Firebase event Jun 2017

228 views

Published on

How we use Firebase database rules and Cloud Functions at Beeline.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Beeline Firebase talk - Firebase event Jun 2017

  1. 1. Launching Beeline with Firebase 15-06-2017
  2. 2. Who is this guy? CTO at beeline.co @chetbox
  3. 3. What is Beeline? We make journeys more enjoyable
  4. 4. What is Beeline?
  5. 5. What is Beeline?
  6. 6. The app
  7. 7. The app
  8. 8. The big challenge ● 3 months ● Android and iOS ● User accounts ● Data storage ● Analytics
  9. 9. Data storage Realm + S3 → ?
  10. 10. Data storage Why Firebase Database? ● Hosted ● Automatic sync to server ● iOS and Android SDK ● Open-source UI libraries
  11. 11. Data storage What’s different? ● NoSQL ● Security enforced by rules.json ● Realtime ● Reactive UI
  12. 12. rules.json { "rules": { ".read": "auth != null", ".write": "auth != null" } }
  13. 13. rules.json { "rules": { "destinations": { "$user_id": { ".validate": "newData.hasChildren(['latitude', 'longitude'])”, ".read": "auth != null && auth.uid == $user_id", ".write": "auth != null && auth.uid == $user_id", ".indexOn": [ "isFavourite" ], "$id": { "latitude": { ".validate": "newData.isNumber() && newData.val() >= -90 && newData.val() <= 90" }, "longitude": { ".validate": "newData.isNumber() && newData.val() >= -180 && newData.val() <= 180" }, "name": { ".validate": "newData.isString()" } ...
  14. 14. Data storage FIREBASE WARNING: set at /destinations/abc/def failed: permission_denied
  15. 15. rules.bolt path /destinations/{user_id}/{id} { latitude: Number; longitude: Number; name: String | Null; } path /destinations/{user_id} { read() {auth && auth.uid == user_id} write() {auth && auth.uid == user_id} }
  16. 16. rules.bolt path /destinations/{user_id}/{id} is Destination; type LatLon { latitude: Number; longitude: Number; } type Destination extends LatLon { name: String | Null; }
  17. 17. rules.bolt type LatLon { latitude: Number; longitude: Number; validate() { latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180 } }
  18. 18. rules.bolt path /destinations/{user_id} { read() { isSignedInUser(user_id) } write() { isSignedInUser(user_id) } } isSignedInUser(user_id) { isSignedIn() && auth.uid === user_id } isSignedIn() { auth != null }
  19. 19. Test your rules!
  20. 20. Test your rules! Targaryen is a former Great House of Westeros and was the ruling royal House of the Seven Kingdoms for three centuries, before it was deposed during Robert's Rebellion and House Baratheon replaced it as the new royal House. The few surviving Targaryens fled into exile. Currently based in Essos, House Targaryen seeks to retake the Seven Kingdoms from House Lannister, who formally replaced House Baratheon as the royal House following the destruction of the Great Sept of Baelor. http://gameofthrones.wikia.com/wiki/House_Targaryen
  21. 21. Test your rules! targaryen = require 'targaryen' rules = require 'rules.json' describe 'Destinations', -> beforeEach -> targaryen.setFirebaseRules rules targaryen.setFirebaseData destinations: queen: buckingham_palace: latitude: 51.5013021 longitude: -0.148308 it 'cannot be read publicly', -> expect targaryen.users.unauthenticated .cannotRead '/destinations'
  22. 22. Test your rules! it 'can be read by owner', -> expect uid: 'queen' .canRead '/destinations/queen' it 'cannot be read by others', -> expect {} .cannotRead '/destinations/queen' expect uid: 'mayor' .cannotRead '/destinations/queen'
  23. 23. Test your rules! it 'can be created by owner', -> expect uid: 'queen' .canWrite '/destinations/queen/trafalgar_square', latitude: 51.5069249 longitude: -0.1297893 it 'cannot be created by others', -> [{}, uid: 'peasant'] .forEach (user) -> expect user .cannotWrite '/destinations/queen/trafalgar_square', latitude: 51.5069249 longitude: -0.1297893
  24. 24. Test your rules! it 'can be re-named by owner', -> expect uid: 'queen' .canWrite '/destinations/queen/buckingham_palace/name', 'Home'
  25. 25. Test your rules! $ firebase-bolt rules.bolt $ node node_modules/jasmine/bin/jasmine.js // package.json { "scripts": { "test": "..." } } $ npm test
  26. 26. Automatic deployment $ npm test bolt: Generating rules.json... Started ............................................................... ............................................................... ............................................................... .......................... 215 specs, 0 failures Finished in 0.5 seconds The command "npm test" exited with 0. $ ./deploy Deploying to 'master' beeline-test-e7288 === Deploying to 'beeline-test-e7288'... i deploying database, functions ✔ database: rules ready to deploy. i starting release process (may take several minutes)... ✔ Deploy complete!
  27. 27. Querying ref.orderBy("date") .startAt("2017-04-16") .endAt("2017-04-23") ref.orderBy("date") .equalTo("2017-04-24") .limitToLast(1)
  28. 28. Cloud functions to the rescue!
  29. 29. Aggregating // functions/index.ts import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; const db = admin.database(); export const all_destinations = functions.database.ref('/destinations/{userId}/{id}') .onWrite((event: functions.Event<functions.database.DeltaSnapshot>) => { return db.ref(`/all-destinations/${event.params.id}`) .set(event.data.val()); }); export const destinations_index = functions.database.ref('/destinations/{userId}/{id}') .onWrite((event: functions.Event<functions.database.DeltaSnapshot>) => { if (event.data.val().name) return db.ref('/destinations-index') .child(event.data.val().name.toLowerCase()) .child(event.params.userId).child(event.params.id) .set(Date.now()); });
  30. 30. Cloud function security sudo rm -rf / Never do this.
  31. 31. Cloud function security import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; admin.initializeApp(Object.assign( {databaseAuthVariableOverride: { uid: "stats@cloudfunction.beeline.co" }, functions.config().firebase ); const db = admin.database();
  32. 32. Cloud function security path /stats/user/{user_id} { read() { isSignedInUser(user_id) } write() { isCloudFunctionUser('stats') } } isCloudFunctionUser(name) { auth.uid === name + '@cloudfunction.beeline.co' }
  33. 33. Cloud function tests ● Use .val() ● Unit test your data logic ● Connect it up in index.js ● Unit test your functions https://firebase.google.com/docs/functions/unit-testing ● Set up error notifications for production https://console.cloud.google.com/errors
  34. 34. Cloud function tests language: node_js node_js: "6" before_script: - npm install - (cd functions && npm install) script: - npm test - (cd functions && npm test) - ./deploy
  35. 35. Automatic deployment #!/bin/bash set -e if [ "$TRAVIS_PULL_REQUEST" == "false" ] ; then if [ "$TRAVIS_BRANCH" == "master" ] ; then FIREBASE_PROJECT="my-staging-project" fi if [ "$TRAVIS_BRANCH" == "release" ] ; then FIREBASE_PROJECT="my-production-project" fi fi if [ -n "$FIREBASE_PROJECT" ] ; then node "node_modules/firebase-tools/bin/firebase" --project "$FIREBASE_PROJECT" --token "$FIREBASE_TOKEN" deploy --message "$TRAVIS_COMMIT") fi
  36. 36. Permissions for special users path /permissions/{user_id} { change_permissions: True | Null; internal_feature_1: True | Null; beta_feature_1: True | Null; write(){ userHasPermission('change_permissions') } } type True extends Boolean { validate(){ this === true } } userHasPermission(permission) { root.permissions[auth.uid][permission] === true }
  37. 37. Automatically save device metadata exports.ride_saveAppAndDeviceInfo = functions.analytics.event('ride_started').onLog( (event: functions.Event<functions.analytics.AnalyticsEvent>) => { const user = event.data.user; const uid = user && user.userId; const rideId = event.data.params && event.data.params.value; if (uid && rideId) { return db.ref(`/rides/${uid}/${rideId}`).update({ app_version: user.appInfo.appVersion, phone_os: user.appInfo.appPlatform, phone_os_version: user.deviceInfo.platformVersion, phone_model: user.deviceInfo.mobileModelName, phone_manufacturer: user.deviceInfo.mobileBrandName }); } }); // Be careful of race conditions
  38. 38. Web dashboard
  39. 39. Web dashboard
  40. 40. Web dashboard
  41. 41. Web dashboard
  42. 42. What we like so far ● Easy to set up ● Use as much as you like ● Realtime DB updates ● Minimal backend code ● Detailed docs
  43. 43. What we like less so far ● A bit of a learning curve ● Lacking examples ● Limited querying ● Developing functions is slow ● Slow to load lots of data ● In flux
  44. 44. What’s next ● More in-depth stats ● Large data → Long term (cheaper) storage ● Emails/push for in-app events
  45. 45. Questions? https://beeline.co @chetbox

×