Tokens should be either completely random, or cryptographically signed data (either asymmetric signature algorithm such as RSA or ECDSA, or message authentication code such as HMAC) using a key only the server knows. In the latter case, everything except the signature can be whatever you want (including simple human-readable text), but there are standard formats with existing libraries and I recommend you use them.
If you want to go the "random data" route, it's sometimes a good idea to store a hash of the token server-side, and hash the user-submitted token before validation (this is an easy way to avoid timing attacks on the check).
It sounds like you're trying to re-invent the "access token + refresh token" model. Leaving aside the probably-irrelevant stuff (why do you have a Node server sitting between your users and a PHP back-end?), it sounds like you're trying to develop some sort of long-term access token.
In theory, this is trivial; just generate a securely random string of bytes - 16 bytes out of
/dev/urandom is fine - and then hex- or base64-encode that and use it as the access token (provided to the client, and the client transmits it with every request). The server maintains a dictionary in memory of user sessions and, upon receiving the token, looks it up in the session dictionary to retrieve the user info. The sessions have an expiration (possibly known to the client, but enforced by the server after which the user needs to actually log in again, but the expiration can be years or decades away if you really want it to. The server should also support invalidating a token (logging out) on demand, before it expires.
In practice, the trivial approach doesn't scale. First, anything that requires storing state on the server gets expensive when you have lots of users, especially if the sessions are long-lived. Second, this breaks down when you switch from a single back-end server to a cluster, because now you either need to cache sessions across the whole cluster and keep them in sync, or you need to route each request to the correct back-end server in the cluster (the one that has the relevant session). Both approaches are possible but both kind of suck.
The common solution, then, is to put as much state as possible on the client rather than the server, but sign it with a key known only to the server so a malicious user can't modify their session state (for example, to impersonate another user). This is typically done using a JSON Web Token, abbreviate JWT; these are a standardized three-part format for tokens that encompasses metadata (issuer, expiration, etc.), session data (user identity, permissions, anything else you want), and a signature over the other two parts (to prevent modification). The great thing about JWTs is that they require no server-side session state or DB lookups at all - if the token is within its validity period and the signature checks out, you can trust all the data in the JWT - so they are great for scaling. The problem with JWTs is that there's no good way to revoke them or dynamically lower their privileges; because the server stores no session state, it can't tell when you want to modify a session or end it prematurely.
The solution to that last problem is to make the JWTs very short-lived - typically a few minutes - and add "refresh tokens" (which is basically the random string mentioned above, stored in the DB on the server side). The refresh token is only used when a JWT is expired, or possibly just when it will expire soon, and serves as a long-term token that can be used to retrieve an updated JWT. While the JWT can be validated without needing a DB lookup, refresh tokens can't; they are stored in the DB along with any other relevant information. However, a user might make hundreds of requests that use the JWT in the time before needing to refresh it, so the load on the DB is still very low compared to a session lookup on every request, and you still avoid the state synchronization issue.
The total process looks like this:
- User visits login page, provides credentials.
- Server validates credentials, generates a refresh token and an access token (probably a JWT) back to the client.
- Client includes the JWT with every request for a few minutes, allowing the server to keep track of session state without needing to store that state between requests.
- Client periodically uses the refresh token to request a new JWT, which will be valid for a few minutes more.
- If the client goes away without logging out, the JWT will expire but the refresh token will remain valid, so the user can resume their session without needing to log in again just by using the refresh token (as in #4).
- If the client logs out (or the session is otherwise terminated for some reason), the refresh token is deleted from the DB. The user could in theory continue using the access token (JWT) for a brief time, but can't refresh it any more and would need to log in again.
With all that said... you should seriously consider using some existing authentication and session management library, rather than trying to build your own. Not all of them are good, but some of them are... and most (though not all) are written by people who didn't need to ask this question. For example, "salted and hashed" is not sufficient for password storage. You need a slow hash function, ideally one explicitly designed for password hashing. Using a well-reviewed open-source library will save you from needing to know all this stuff, and avoid common pitfalls, while also saving you development time.
At the very least, if you do use JWTs, use a library for them. Generating and checking random tokens isn't too hard to do securely (though ideally you do want to avoid things like timing attacks in the string comparison), but more complex token types with digital signatures and so on should be handled by library code. Honestly, though, if you have to ask "completely random" vs. "a combination of letters+numbers", you should let other people handle this.