O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

Launching Beeline with Firebase

160 visualizações

Publicada em

Talk from Mobile Era, Oslo, Norway 2017
https://beeline.co

Publicada em: Tecnologia
  • Seja o primeiro a comentar

  • Seja a primeira pessoa a gostar disto

Launching Beeline with Firebase

  1. 1. Launching Beeline with Firebase Oct 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. Prototype
  6. 6. What is Beeline?
  7. 7. What is Beeline?
  8. 8. The app
  9. 9. The big challenge ● 3 months ● Android and iOS ● User accounts ● Data storage ● Analytics
  10. 10. User accounts Just use Firebase Authentication
  11. 11. Data storage
  12. 12. Data storage Realm + S3 → ?
  13. 13. Data storage Why Firebase Database? ● Hosted ● Automatic sync to server ● iOS and Android SDK ● Open-source UI libraries
  14. 14. Data storage What’s different? ● NoSQL ● Security enforced by rules.json ● Realtime ● Reactive UI
  15. 15. rules.json { "rules": { ".read": "auth != null", ".write": "auth != null" } }
  16. 16. 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()" } ...
  17. 17. Data storage FIREBASE WARNING: set at /destinations/abc/def failed: permission_denied
  18. 18. 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} }
  19. 19. rules.bolt path /destinations/{user_id}/{id} is Destination; type LatLon { latitude: Number; longitude: Number; } type Destination extends LatLon { name: String | Null; }
  20. 20. rules.bolt type LatLon { latitude: Number; longitude: Number; validate() { latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180 } }
  21. 21. 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 }
  22. 22. Test your rules!
  23. 23. 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
  24. 24. 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'
  25. 25. Test your rules! it 'can be read by owner', -> expect uid: 'queen' .canRead '/destinations/queen' it 'cannot be read by others', -> expect targaryen.users.unauthenticated .cannotRead '/destinations/queen' expect uid: 'mayor' .cannotRead '/destinations/queen'
  26. 26. Test your rules! it 'can be created by owner', -> expect uid: 'queen' .canWrite '/destinations/queen/palace, latitude: 51.501364 longitude: -0.14189
  27. 27. Test your rules! // package.json { "scripts": { "test": "firebase-bolt rules.bolt && node node_modules/jasmine/bin/jasmine.js" } } $ npm test
  28. 28. 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. $ firebase --project beeline-test-e7288 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!
  29. 29. .travis.yml language: node_js node_js: "6" install: - npm install script: - npm test deploy: - on: branch: master skip_cleanup: true script: firebase --project beeline-test-e7288 --token "$FIREBASE_TOKEN" deploy
  30. 30. Android/iOS project structure - app - src - debug - google-services.json - release - google-services.json - Beeline - GoogleService-Info.plist - GoogleService-Info-Test.plist
  31. 31. Android @IgnoreExtraProperties data class Destination( val latitude: Double = 0.0, val longitude: Double = 0.0, val name: String? = null, ) val ref = FirebaseDatabase.getInstance() .getReference("/destinations/user1/dest1") // Write data ref.setValue(Destination(51.5, 0))
  32. 32. Android // Read data ref.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val destination = snapshot.getValue(Destination::class.java) // ... } override fun onCancelled(error: DatabaseError) {} })
  33. 33. iOS // Write data ref.setValue([ "latitude": 51.5, "longitude": 0 ])
  34. 34. Querying ref.orderBy("date-string") .startAt("2017-04-16") .endAt("2017-04-23") ref.orderBy("date-string") .equalTo("2017-04-24") .limitToLast(1)
  35. 35. Cloud functions to the rescue!
  36. 36. Which language?
  37. 37. 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) => db.ref(`/all-destinations/${event.params.id}`) .set(event.data.val()); );
  38. 38. Cloud function security sudo rm -rf / Never do this.
  39. 39. 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();
  40. 40. Cloud function security path /stats/user/{user_id} { read() { isSignedInUser(user_id) } write() { isCloudFunctionUser('stats') } } isCloudFunctionUser(name) { auth.uid === name + '@cloudfunction.beeline.co' }
  41. 41. Cloud function testing tips ● Use .val() ● Unit test your data logic ● Unit test your functions (fiddly) https://firebase.google.com/docs/functions/unit-testing ● Set up error notifications for production https://console.cloud.google.com/errors
  42. 42. .travis.yml language: node_js node_js: "6" install: - npm install - (cd functions && npm install) script: - npm test - (cd functions && npm test) deploy: - on: branch: master skip_cleanup: true script: firebase --project beeline-test-e7288 --token "$FIREBASE_TOKEN" deploy
  43. 43. And then?
  44. 44. Get fancy ● User permissions as database rules ● Index your data ● Automate with analytics events ● Build web dashboards
  45. 45. Permissions for special users path /permissions/{user_id} { change_permissions: Boolean | Null; internal_feature_1: Boolean | Null; beta_feature_1: Boolean | Null; write(){ userHasPermission('change_permissions') } } userHasPermission(permission) { root.permissions[auth.uid][permission] === true }
  46. 46. Indexing // index /destinations by name export const destinations_index = functions.database.ref('/destinations/{userId}/{id}') .onWrite((event) => { 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()); });
  47. 47. 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
  48. 48. Web dashboard
  49. 49. Web dashboard
  50. 50. Web admin
  51. 51. Web dashboard
  52. 52. Web dashboard
  53. 53. What we like so far ● Easy to set up ● Easy to integrate ● Realtime DB updates ● Minimal backend code ● Detailed docs
  54. 54. 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 ● “Open source” ● In flux
  55. 55. What’s next ● More in-depth stats ● Large data → Long term (cheaper) storage ● Emails/push for in-app events ● Cloud Firestore?
  56. 56. Questions? https://beeline.co @chetbox

×